dsh-subscriptions 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +31 -5
  2. package/lib/auth/device-flow.js +6 -3
  3. package/lib/auth/oauth-flow.js +41 -8
  4. package/lib/auth/rpc.d.ts +73 -2
  5. package/lib/auth/rpc.js +53 -3
  6. package/lib/client/ExternalUsageCards.js +1 -1
  7. package/lib/client/OllamaCard.d.ts +13 -0
  8. package/lib/client/OllamaCard.js +76 -0
  9. package/lib/client/OllamaSettingsManager.d.ts +24 -0
  10. package/lib/client/OllamaSettingsManager.js +246 -0
  11. package/lib/client/ProviderModelEditor.js +4 -2
  12. package/lib/client/SubscriptionUsageBadge.d.ts +1 -1
  13. package/lib/client/SubscriptionUsageBadge.js +3 -2
  14. package/lib/client/SubscriptionsSection.d.ts +2 -74
  15. package/lib/client/SubscriptionsSection.js +4 -67
  16. package/lib/client/UsageMeter.js +11 -7
  17. package/lib/client/locales.d.ts +68 -0
  18. package/lib/client/locales.js +77 -0
  19. package/lib/client/ollama-draft.d.ts +229 -0
  20. package/lib/client/ollama-draft.js +246 -0
  21. package/lib/client/usage-pace.d.ts +7 -0
  22. package/lib/client/usage-pace.js +15 -0
  23. package/lib/client.js +1385 -244
  24. package/lib/client.js.map +1 -1
  25. package/lib/index.d.ts +25 -0
  26. package/lib/index.js +11060 -9733
  27. package/lib/model-defaults.d.ts +4 -5
  28. package/lib/model-defaults.js +18 -14
  29. package/lib/provider-settings.d.ts +2 -1
  30. package/lib/provider-settings.js +6 -4
  31. package/lib/providers/antigravity.d.ts +0 -2
  32. package/lib/providers/antigravity.js +5 -9
  33. package/lib/providers/claude.d.ts +14 -1
  34. package/lib/providers/claude.js +25 -13
  35. package/lib/providers/codex-search.js +5 -2
  36. package/lib/providers/codex.js +7 -7
  37. package/lib/providers/common.d.ts +27 -0
  38. package/lib/providers/common.js +70 -0
  39. package/lib/providers/copilot.js +3 -3
  40. package/lib/providers/cursor-adapter.d.ts +35 -0
  41. package/lib/providers/cursor-adapter.js +245 -4
  42. package/lib/providers/cursor-auth.js +11 -4
  43. package/lib/providers/cursor-usage.js +2 -1
  44. package/lib/providers/external-usage-controller.d.ts +9 -2
  45. package/lib/providers/external-usage-controller.js +19 -5
  46. package/lib/providers/external-usage.d.ts +8 -0
  47. package/lib/providers/external-usage.js +93 -8
  48. package/lib/providers/grok.js +14 -9
  49. package/lib/providers/minimax-usage.js +2 -1
  50. package/lib/providers/ollama-settings.d.ts +90 -0
  51. package/lib/providers/ollama-settings.js +197 -0
  52. package/lib/providers/ollama-web.d.ts +46 -0
  53. package/lib/providers/ollama-web.js +213 -0
  54. package/lib/providers/ollama.d.ts +107 -0
  55. package/lib/providers/ollama.js +545 -0
  56. package/lib/tools/image-generate.js +2 -1
  57. package/lib/tools/video-generate.d.ts +0 -5
  58. package/lib/tools/video-generate.js +25 -14
  59. package/lib/tools/x-search.js +2 -2
  60. package/lib/translate/resolved.d.ts +5 -2
  61. package/lib/translate/resolved.js +5 -2
  62. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -108,6 +108,7 @@ const en = {
108
108
  usageRefresh: "Refresh",
109
109
  usageLoading: "Loading usage…",
110
110
  usageEmpty: "No usage windows reported.",
111
+ usageUnsupported: "This endpoint has no usage surface.",
111
112
  usageError: "Usage lookup failed: {message}",
112
113
  usageSession: "5-hour window",
113
114
  usageWeekly: "Weekly",
@@ -233,7 +234,40 @@ const en = {
233
234
  speedFast: "Fast",
234
235
  speedFastDescription: "1.5x speed, more usage",
235
236
  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"
237
+ commandFastUnavailable: "The current model has no fast tier; /fast only works on Codex models whose catalog advertises one",
238
+ ollamaTitle: "Ollama Cloud",
239
+ ollamaStatus: "{count} model(s) in the picker",
240
+ ollamaStatusNone: "No models configured",
241
+ ollamaWebOn: "Web search/fetch enabled",
242
+ ollamaWebOff: "Web search/fetch off",
243
+ ollamaManageTitle: "Manage Ollama Cloud",
244
+ 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.",
245
+ ollamaFetch: "Fetch cloud models",
246
+ ollamaFetching: "Fetching…",
247
+ ollamaKeyPlaceholder: "One-time API key (optional)",
248
+ ollamaKeyHint: "A key typed here is used for this fetch only and is never saved. Leave it blank to use the stored credential.",
249
+ ollamaFetchFailed: "Could not fetch cloud models: {message}",
250
+ ollamaCandidatesCount: "{count} returned",
251
+ ollamaCandidatesEmpty: "No cloud models returned.",
252
+ ollamaModelsEmpty: "No models configured. Fetch the cloud list and tick the ones to keep.",
253
+ ollamaModelsLoadFailed: "Could not load Ollama settings: {message}",
254
+ ollamaModelsSaveFailed: "Could not save Ollama settings: {message}",
255
+ ollamaConfigureHint: "No API key found at {ref}. Add it to the credentials file (~/.dsh/.credentials.yaml).",
256
+ ollamaDisplayName: "Display name",
257
+ ollamaRowDetails: "Details",
258
+ ollamaEffortsUnknown: "This model reasons; its level set will come from live discovery.",
259
+ ollamaMoveUp: "Move up",
260
+ ollamaMoveDown: "Move down",
261
+ ollamaBaseURL: "Base URL",
262
+ ollamaApiKeyRef: "API key reference",
263
+ 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.",
264
+ ollamaWebHint: "Enables the Ollama Cloud web search and fetch providers. The host still has to select them before the web tools use them.",
265
+ ollamaCatalogOverride: "A profile pins this route's model list; the picker ignores store models until that override is removed.",
266
+ ollamaResetDefaults: "Restore deployment defaults",
267
+ ollamaResetConfirm: "Clear every saved Ollama override? The profile values and built-in defaults apply again.",
268
+ ollamaCapVision: "vision",
269
+ ollamaCapThinking: "thinking",
270
+ ollamaContext: "context {tokens}"
237
271
  };
238
272
  /** zh strings, one per {@link en} key. */
239
273
  const zh = {
@@ -309,6 +343,7 @@ const zh = {
309
343
  usageRefresh: "刷新",
310
344
  usageLoading: "用量加载中…",
311
345
  usageEmpty: "服务商未返回任何用量窗口。",
346
+ usageUnsupported: "该端点不提供用量查询。",
312
347
  usageError: "用量查询失败:{message}",
313
348
  usageSession: "5 小时窗口",
314
349
  usageWeekly: "每周",
@@ -434,7 +469,40 @@ const zh = {
434
469
  speedFast: "快速",
435
470
  speedFastDescription: "约 1.5 倍速度,消耗更多用量",
436
471
  commandFast: "切换 Codex 速度档(标准/快速)",
437
- commandFastUnavailable: "当前模型不支持快速档;/fast 仅对目录声明了 fast tier 的 Codex 模型可用"
472
+ commandFastUnavailable: "当前模型不支持快速档;/fast 仅对目录声明了 fast tier 的 Codex 模型可用",
473
+ ollamaTitle: "Ollama Cloud",
474
+ ollamaStatus: "选择器中 {count} 个模型",
475
+ ollamaStatusNone: "尚未配置模型",
476
+ ollamaWebOn: "网页搜索/抓取已启用",
477
+ ollamaWebOff: "网页搜索/抓取已关闭",
478
+ ollamaManageTitle: "管理 Ollama Cloud",
479
+ ollamaManageHint: "选择此路由提供的云端模型后保存。模型选择器按此顺序显示这份列表;API 密钥本身仍保存在 DSH 凭据中。",
480
+ ollamaFetch: "拉取云端模型",
481
+ ollamaFetching: "拉取中…",
482
+ ollamaKeyPlaceholder: "一次性 API 密钥(可选)",
483
+ ollamaKeyHint: "在此输入的密钥仅用于本次拉取,不会保存。留空则使用已存凭据。",
484
+ ollamaFetchFailed: "拉取云端模型失败:{message}",
485
+ ollamaCandidatesCount: "返回 {count} 个",
486
+ ollamaCandidatesEmpty: "未返回任何云端模型。",
487
+ ollamaModelsEmpty: "尚未配置模型。请拉取云端列表并勾选需要保留的模型。",
488
+ ollamaModelsLoadFailed: "Ollama 设置加载失败:{message}",
489
+ ollamaModelsSaveFailed: "Ollama 设置保存失败:{message}",
490
+ ollamaConfigureHint: "未在 {ref} 找到 API 密钥。请在凭据文件(~/.dsh/.credentials.yaml)中添加。",
491
+ ollamaDisplayName: "显示名",
492
+ ollamaRowDetails: "详情",
493
+ ollamaEffortsUnknown: "该模型支持思考,推理档位将由实时发现补充。",
494
+ ollamaMoveUp: "上移",
495
+ ollamaMoveDown: "下移",
496
+ ollamaBaseURL: "基础 URL",
497
+ ollamaApiKeyRef: "API 密钥引用",
498
+ ollamaConnectionHint: "基础 URL 用于此路由的请求与凭据查询;引用是 DSH 凭据的名称,不是密钥本身。请求会把解析出的密钥发往该地址,请只填写可信主机。",
499
+ ollamaWebHint: "启用 Ollama Cloud 的网页搜索与抓取能力。宿主仍需选择它们,网页工具才会使用。",
500
+ ollamaCatalogOverride: "配置档案固定了此路由的模型列表;在移除该覆盖前,选择器会忽略存储中的模型。",
501
+ ollamaResetDefaults: "恢复部署默认值",
502
+ ollamaResetConfirm: "清除全部已保存的 Ollama 覆盖项?将重新使用配置档案值与内置默认值。",
503
+ ollamaCapVision: "视觉",
504
+ ollamaCapThinking: "思考",
505
+ ollamaContext: "上下文 {tokens}"
438
506
  };
439
507
 
440
508
  //#endregion
@@ -521,8 +589,10 @@ function mergeAccountChanges(latest, changes) {
521
589
 
522
590
  //#endregion
523
591
  //#region src/client/ProviderModelEditor.tsx
524
- const border$1 = "0.5px solid var(--dsw-alias-border-l2, #ddd)";
525
- const actions$1 = {
592
+ /** Providers whose runtime honours a local context-window override. */
593
+ const supportsContext = (provider) => provider === "codex" || provider === "claude";
594
+ const border$2 = "0.5px solid var(--dsw-alias-border-l2, #ddd)";
595
+ const actions$2 = {
526
596
  display: "flex",
527
597
  flexWrap: "wrap",
528
598
  gap: 8,
@@ -595,7 +665,7 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
595
665
  setError("");
596
666
  const { accounts: _accounts,...settings } = {
597
667
  ...draft,
598
- ...provider === "codex" ? { contextWindows: windows } : {}
668
+ ...supportsContext(provider) ? { contextWindows: windows } : {}
599
669
  };
600
670
  return {
601
671
  settings,
@@ -625,7 +695,7 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
625
695
  const selected = new Set(draft.visibleModels ?? allModels.map((model) => model.id));
626
696
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
627
697
  style: {
628
- borderTop: border$1,
698
+ borderTop: border$2,
629
699
  marginTop: 12,
630
700
  paddingTop: 12
631
701
  },
@@ -648,7 +718,7 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
648
718
  children: error
649
719
  }),
650
720
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
651
- style: actions$1,
721
+ style: actions$2,
652
722
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
653
723
  type: "button",
654
724
  className: "dsh-subscription-quiet",
@@ -688,7 +758,7 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
688
758
  t("modelsAutomatic")
689
759
  ] }),
690
760
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
691
- style: actions$1,
761
+ style: actions$2,
692
762
  children: [
693
763
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
694
764
  style: { flex: "1 1 180px" },
@@ -724,7 +794,7 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
724
794
  },
725
795
  children: [models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
726
796
  style: {
727
- borderBottom: border$1,
797
+ borderBottom: border$2,
728
798
  padding: "8px 2px",
729
799
  display: "grid",
730
800
  gap: 8
@@ -750,9 +820,9 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
750
820
  !known.has(model.id) && ` (${t("modelsUnavailable")})`
751
821
  ]
752
822
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
753
- style: actions$1,
823
+ style: actions$2,
754
824
  children: [(model.efforts?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
755
- style: actions$1,
825
+ style: actions$2,
756
826
  children: [t("modelDefaultsTitle"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
757
827
  "aria-label": `${model.name} ${t("modelDefaultsTitle")}`,
758
828
  value: efforts[model.id] ?? "",
@@ -784,10 +854,10 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
784
854
  }, effort.id))
785
855
  ]
786
856
  })]
787
- }), provider === "codex" && model.maxContextWindow !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
788
- style: actions$1,
857
+ }), supportsContext(provider) && model.maxContextWindow !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
858
+ style: actions$2,
789
859
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
790
- style: actions$1,
860
+ style: actions$2,
791
861
  children: [t("modelsContext"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
792
862
  style: { width: 140 },
793
863
  inputMode: "numeric",
@@ -810,7 +880,7 @@ const ProviderModelEditor = (0, react.forwardRef)(function ProviderModelEditor$1
810
880
  })]
811
881
  }, model.id)), models.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("modelDefaultsFilterEmpty", { query }) })]
812
882
  }),
813
- provider === "codex" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("modelsContextHint") }),
883
+ supportsContext(provider) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("modelsContextHint") }),
814
884
  catalog.tools.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
815
885
  style: {
816
886
  display: "grid",
@@ -1054,13 +1124,13 @@ const subscriptionChromeCss = `
1054
1124
 
1055
1125
  //#endregion
1056
1126
  //#region src/client/ProviderAccountManager.tsx
1057
- const border = "0.5px solid var(--dsw-alias-border-l2)";
1058
- const stack = {
1127
+ const border$1 = "0.5px solid var(--dsw-alias-border-l2)";
1128
+ const stack$1 = {
1059
1129
  display: "grid",
1060
1130
  gap: 12,
1061
1131
  minWidth: 0
1062
1132
  };
1063
- const actions = {
1133
+ const actions$1 = {
1064
1134
  display: "flex",
1065
1135
  flexWrap: "wrap",
1066
1136
  gap: 8,
@@ -1151,8 +1221,8 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1151
1221
  if (alive.current) onClose();
1152
1222
  } catch (error$1) {
1153
1223
  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 }));
1224
+ const message$3 = error$1 instanceof Error ? error$1.message : String(error$1);
1225
+ setSaveError((savedEfforts ? t("modelsPartialSave") + " " : "") + t("accountsSaveFailed", { message: message$3 }));
1156
1226
  }
1157
1227
  } finally {
1158
1228
  saveLock.current = false;
@@ -1176,13 +1246,13 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1176
1246
  },
1177
1247
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: providerSettingsCss }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1178
1248
  style: {
1179
- ...stack,
1249
+ ...stack$1,
1180
1250
  padding: 20
1181
1251
  },
1182
1252
  children: [
1183
1253
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1184
1254
  style: {
1185
- ...actions,
1255
+ ...actions$1,
1186
1256
  justifyContent: "space-between"
1187
1257
  },
1188
1258
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
@@ -1219,7 +1289,7 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1219
1289
  catalog && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
1220
1290
  disabled: saving || loading,
1221
1291
  style: {
1222
- ...stack,
1292
+ ...stack$1,
1223
1293
  border: 0,
1224
1294
  margin: 0,
1225
1295
  padding: 0
@@ -1231,7 +1301,7 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1231
1301
  }),
1232
1302
  provider === "cursor-subscription" && catalog.accounts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
1233
1303
  style: {
1234
- border,
1304
+ border: border$1,
1235
1305
  borderRadius: "var(--dsw-radius-lg, 12px)",
1236
1306
  padding: 14
1237
1307
  },
@@ -1249,8 +1319,8 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1249
1319
  const models = accountModelRows(account, preferences);
1250
1320
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
1251
1321
  style: {
1252
- ...stack,
1253
- border,
1322
+ ...stack$1,
1323
+ border: border$1,
1254
1324
  borderRadius: "var(--dsw-radius-lg, 12px)",
1255
1325
  padding: 14,
1256
1326
  margin: 0
@@ -1270,7 +1340,7 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1270
1340
  }),
1271
1341
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1272
1342
  style: {
1273
- ...stack,
1343
+ ...stack$1,
1274
1344
  gap: 6
1275
1345
  },
1276
1346
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1327,7 +1397,7 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1327
1397
  ]
1328
1398
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1329
1399
  style: {
1330
- ...stack,
1400
+ ...stack$1,
1331
1401
  marginTop: 12
1332
1402
  },
1333
1403
  children: [
@@ -1350,7 +1420,7 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1350
1420
  t("accountsModelsAutomatic")
1351
1421
  ] }),
1352
1422
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1353
- style: actions,
1423
+ style: actions$1,
1354
1424
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1355
1425
  type: "button",
1356
1426
  onClick: () => edit(account.key, {
@@ -1369,7 +1439,7 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1369
1439
  }),
1370
1440
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1371
1441
  style: {
1372
- ...stack,
1442
+ ...stack$1,
1373
1443
  gap: 8,
1374
1444
  maxHeight: 240,
1375
1445
  overflowY: "auto"
@@ -1416,9 +1486,9 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1416
1486
  }),
1417
1487
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
1418
1488
  style: {
1419
- ...stack,
1489
+ ...stack$1,
1420
1490
  gap: 8,
1421
- borderTop: border,
1491
+ borderTop: border$1,
1422
1492
  padding: "14px 0 8px",
1423
1493
  position: "sticky",
1424
1494
  bottom: 0,
@@ -1429,7 +1499,7 @@ function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
1429
1499
  children: saveError
1430
1500
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1431
1501
  style: {
1432
- ...actions,
1502
+ ...actions$1,
1433
1503
  justifyContent: "flex-end"
1434
1504
  },
1435
1505
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -1704,6 +1774,19 @@ function usageBarColor(used, elapsed, preset = "standard", fresh = true) {
1704
1774
  const state = usageColorState(used, elapsed, preset, fresh);
1705
1775
  return state === "red" ? "var(--dsw-alias-state-error-primary)" : state === "yellow" ? "var(--dsw-alias-state-warn-label)" : "var(--dsw-alias-state-success-primary)";
1706
1776
  }
1777
+ /**
1778
+ * Marker position from the window's display-only assumed start
1779
+ * (`displayStartsAt`): the reading and its age are irrelevant, exactly
1780
+ * like the verified marker. Returns undefined when the window carries no
1781
+ * assumed start or its reset has passed.
1782
+ */
1783
+ function displayStartPercent(window$1, now = Date.now()) {
1784
+ const end = window$1.resetsAt;
1785
+ const start = window$1.displayStartsAt;
1786
+ if (end === void 0 || !Number.isFinite(end) || end <= now) return void 0;
1787
+ if (start === void 0 || !Number.isFinite(start) || start >= end || start > now) return void 0;
1788
+ return Math.min(100, Math.max(0, (now - start) / (end - start) * 100));
1789
+ }
1707
1790
 
1708
1791
  //#endregion
1709
1792
  //#region src/client/usage-color-preferences.ts
@@ -1765,6 +1848,7 @@ function UsageMeter({ window: quota, t, style, observedAt, stale }) {
1765
1848
  stale
1766
1849
  }, current);
1767
1850
  const elapsed = elapsedPercent(quota, current);
1851
+ const marker = elapsed ?? displayStartPercent(quota, current);
1768
1852
  const pace = fresh ? elapsed : void 0;
1769
1853
  const state = usageColorState(quota.usedPercent, pace, preset, fresh);
1770
1854
  const lead = pace === void 0 ? 0 : Math.round(used - pace);
@@ -1774,7 +1858,7 @@ function UsageMeter({ window: quota, t, style, observedAt, stale }) {
1774
1858
  remaining: Math.round(100 - used)
1775
1859
  }) : t("usageMeterInvalid"),
1776
1860
  !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) }),
1861
+ marker === void 0 ? "" : t("usageMeterPace", { elapsed: Math.round(marker) }),
1778
1862
  pace === void 0 ? "" : lead > 0 ? t("usageMeterPaceAhead", { points: lead }) : lead < 0 ? t("usageMeterPaceBehind", { points: -lead }) : t("usageMeterPaceEven"),
1779
1863
  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
1864
  ].filter(Boolean).join(" · ");
@@ -1797,12 +1881,12 @@ function UsageMeter({ window: quota, t, style, observedAt, stale }) {
1797
1881
  borderRadius: "inherit",
1798
1882
  width: `${used}%`,
1799
1883
  background: usageBarColor(quota.usedPercent, pace, preset, fresh)
1800
- } }), elapsed !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1884
+ } }), marker !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1801
1885
  "aria-hidden": "true",
1802
1886
  "data-usage-time-marker": true,
1803
1887
  style: {
1804
1888
  position: "absolute",
1805
- left: `clamp(0px, calc(${elapsed}% - 1px), calc(100% - 2px))`,
1889
+ left: `clamp(0px, calc(${marker}% - 1px), calc(100% - 2px))`,
1806
1890
  top: 0,
1807
1891
  bottom: 0,
1808
1892
  width: 2,
@@ -1981,6 +2065,10 @@ function ExternalUsageCards({ rpc, t }) {
1981
2065
  style: subscriptionCardStyles.error,
1982
2066
  children: t("usageError", { message: errors[id] })
1983
2067
  }),
2068
+ snapshot !== void 0 && snapshot.supported === false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2069
+ style: subscriptionCardStyles.status,
2070
+ children: t("usageUnsupported")
2071
+ }),
1984
2072
  snapshot?.windows?.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1985
2073
  style: subscriptionCardStyles.status,
1986
2074
  children: t("usageEmpty")
@@ -2022,7 +2110,7 @@ function ExternalUsageCards({ rpc, t }) {
2022
2110
 
2023
2111
  //#endregion
2024
2112
  //#region src/client/CursorCard.tsx
2025
- function message(error) {
2113
+ function message$2(error) {
2026
2114
  return error instanceof Error ? error.message : String(error);
2027
2115
  }
2028
2116
  /** Cursor's single connected account, rendered with the same card and Manage dialog as the other providers. */
@@ -2047,7 +2135,7 @@ function CursorCard({ rpc, t }) {
2047
2135
  if (value.authenticated) setAuthorizeUrl(void 0);
2048
2136
  if (value.error) setError(value.error);
2049
2137
  } catch (failure) {
2050
- setError(message(failure));
2138
+ setError(message$2(failure));
2051
2139
  }
2052
2140
  }, [rpc]);
2053
2141
  const refreshUsage = (0, react.useCallback)(async () => {
@@ -2060,7 +2148,7 @@ function CursorCard({ rpc, t }) {
2060
2148
  setError(void 0);
2061
2149
  window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
2062
2150
  } catch (failure) {
2063
- setError(message(failure));
2151
+ setError(message$2(failure));
2064
2152
  setUsageStale(true);
2065
2153
  } finally {
2066
2154
  setLoading(false);
@@ -2087,7 +2175,7 @@ function CursorCard({ rpc, t }) {
2087
2175
  window.open(value.authorizeUrl, "_blank", "noopener");
2088
2176
  await refreshStatus();
2089
2177
  } catch (failure) {
2090
- setError(message(failure));
2178
+ setError(message$2(failure));
2091
2179
  }
2092
2180
  };
2093
2181
  const cancel = async () => {
@@ -2096,7 +2184,7 @@ function CursorCard({ rpc, t }) {
2096
2184
  setAuthorizeUrl(void 0);
2097
2185
  await refreshStatus();
2098
2186
  } catch (failure) {
2099
- setError(message(failure));
2187
+ setError(message$2(failure));
2100
2188
  }
2101
2189
  };
2102
2190
  const logout = async () => {
@@ -2111,7 +2199,7 @@ function CursorCard({ rpc, t }) {
2111
2199
  setUsageStale(false);
2112
2200
  await refreshStatus();
2113
2201
  } catch (failure) {
2114
- setError(message(failure));
2202
+ setError(message$2(failure));
2115
2203
  }
2116
2204
  };
2117
2205
  const connected = status?.authenticated === true;
@@ -2265,220 +2353,1267 @@ function CursorCard({ rpc, t }) {
2265
2353
  }
2266
2354
 
2267
2355
  //#endregion
2268
- //#region src/client/reset-credits-view.tsx
2269
- const STATE_KEYS = {
2270
- available: "resetCreditAvailable",
2271
- notUsable: "resetCreditNotUsable",
2272
- paused: "resetCreditPaused",
2273
- exhausted: "resetCreditExhausted",
2274
- expired: "resetCreditExpired"
2275
- };
2356
+ //#region src/client/ollama-draft.ts
2276
2357
  /**
2277
- * Classify one grant. Expired outranks every other flag. A future
2278
- * `cooldownUntil` or `usableNow: false` is not usable even when `claimable`
2279
- * is true. `claimable` does not change the word: it is not an action.
2358
+ * Append the checked candidates to the draft's model list. Existing rows are
2359
+ * never replaced or reordered, ids already present are skipped, and adopted
2360
+ * rows keep the candidate order. Optional fields are set only when the
2361
+ * candidate advertises a value, because `exactOptionalPropertyTypes` (and the
2362
+ * store) must not see an explicit `undefined`.
2363
+ * @param current - the draft list before adoption.
2364
+ * @param candidates - the cloud list as fetched, in display order.
2365
+ * @param selectedIds - ids the user checked.
2366
+ * @returns a new list; the input array is not mutated.
2280
2367
  */
2281
- function resetCreditState(credit, now) {
2282
- if (credit.expiresAt !== void 0 && credit.expiresAt <= now) return "expired";
2283
- if (credit.paused === true) return "paused";
2284
- if (credit.resetsLeft === 0) return "exhausted";
2285
- if (credit.cooldownUntil !== void 0 && credit.cooldownUntil > now || credit.usableNow === false) return "notUsable";
2286
- return "available";
2368
+ function adoptCandidates(current, candidates, selectedIds) {
2369
+ const models = [...current ?? []];
2370
+ const present = new Set(models.map((model) => model.id));
2371
+ const selected = new Set(selectedIds);
2372
+ for (const candidate of candidates) {
2373
+ if (!selected.has(candidate.id) || present.has(candidate.id)) continue;
2374
+ present.add(candidate.id);
2375
+ models.push({
2376
+ id: candidate.id,
2377
+ name: candidate.name,
2378
+ ...candidate.contextWindow === void 0 ? {} : { contextWindow: candidate.contextWindow },
2379
+ ...candidate.vision ? { vision: true } : {},
2380
+ ...candidate.thinking ? { thinking: true } : {},
2381
+ ...candidate.efforts.length ? { efforts: [...candidate.efforts] } : {}
2382
+ });
2383
+ }
2384
+ return models;
2287
2385
  }
2288
2386
  /**
2289
- * Headline count. Codex passes one entry per credit, so the count is the
2290
- * list length. Claude passes one entry per grant, so the count is the sum of
2291
- * `resetsLeft` on grants that are neither expired nor paused. A missing
2292
- * `resetsLeft` adds nothing; the row says the count is unavailable.
2387
+ * Swap one model with its neighbour. An id that is absent or a move past
2388
+ * either end returns the same order.
2389
+ * @param models - the current list.
2390
+ * @param id - the row to move.
2391
+ * @param delta - `-1` moves up, `1` moves down.
2392
+ * @returns a new list; the input array is not mutated.
2293
2393
  */
2294
- function resetCreditsAvailableCount(credits, now, mode) {
2295
- if (mode === "credits") return credits.length;
2296
- let sum = 0;
2297
- for (const credit of credits) {
2298
- const state = resetCreditState(credit, now);
2299
- if (state === "expired" || state === "paused") continue;
2300
- if (typeof credit.resetsLeft === "number" && Number.isFinite(credit.resetsLeft) && credit.resetsLeft > 0) sum += credit.resetsLeft;
2394
+ function moveModel(models, id, delta) {
2395
+ const next = [...models];
2396
+ const index = next.findIndex((model) => model.id === id);
2397
+ const target = index + delta;
2398
+ if (index === -1 || target < 0 || target >= next.length) return next;
2399
+ const moved = next[index];
2400
+ next[index] = next[target];
2401
+ next[target] = moved;
2402
+ return next;
2403
+ }
2404
+ /**
2405
+ * Drop one model from the list.
2406
+ * @param models - the current list.
2407
+ * @param id - the row to drop; an unknown id leaves the list unchanged.
2408
+ * @returns a new list; the input array is not mutated.
2409
+ */
2410
+ function removeModel(models, id) {
2411
+ return models.filter((model) => model.id !== id);
2412
+ }
2413
+ /**
2414
+ * Short context-window label for a model row.
2415
+ * @param contextWindow - the advertised window in tokens.
2416
+ * @returns `'1M'`, `'262K'`, the bare number below 1000, or `undefined` when unknown.
2417
+ */
2418
+ function contextLabel(contextWindow) {
2419
+ if (contextWindow === void 0) return void 0;
2420
+ if (contextWindow < 1e3) return String(contextWindow);
2421
+ if (contextWindow >= 1e6) {
2422
+ const millions = contextWindow / 1e6;
2423
+ return `${Number.isInteger(millions) ? millions : Number(millions.toFixed(1))}M`;
2301
2424
  }
2302
- return sum;
2425
+ return `${Math.round(contextWindow / 1e3)}K`;
2303
2426
  }
2304
- function expiryText(t, credit) {
2305
- return credit.expiresAt === void 0 ? t("resetCreditExpiryUnknown") : t("resetCreditExpires", { date: new Date(credit.expiresAt).toLocaleString() });
2427
+ /**
2428
+ * Rename one model row. An empty display name deletes the field so the
2429
+ * validator keeps accepting the draft; a blank id leaves the list unchanged.
2430
+ * @param models - the current list.
2431
+ * @param id - the row to rename; an unknown id leaves the list unchanged.
2432
+ * @param name - the new display name; whitespace trims before storing.
2433
+ * @returns a new list; the input array is not mutated.
2434
+ */
2435
+ function renameModel(models, id, name) {
2436
+ return models.map((model) => {
2437
+ if (model.id !== id) return model;
2438
+ const next = { ...model };
2439
+ if (name === "") delete next.name;
2440
+ else next.name = name;
2441
+ return next;
2442
+ });
2306
2443
  }
2307
- function remainingText(t, credit) {
2308
- const left = credit.resetsLeft;
2309
- const total = credit.resetsTotal;
2310
- const hasLeft = typeof left === "number" && Number.isFinite(left);
2311
- if (hasLeft && typeof total === "number" && Number.isFinite(total)) return t("resetCreditRemaining", {
2312
- left,
2313
- total
2444
+ /**
2445
+ * Set or clear one model row's default reasoning effort. An empty choice
2446
+ * deletes the field so the resolver falls back to the vendor or family
2447
+ * default.
2448
+ * @param models - the current list.
2449
+ * @param id - the row to edit; an unknown id leaves the list unchanged.
2450
+ * @param effort - the chosen level, or `''` to clear.
2451
+ * @returns a new list; the input array is not mutated.
2452
+ */
2453
+ function setModelEffort(models, id, effort) {
2454
+ return models.map((model) => {
2455
+ if (model.id !== id) return model;
2456
+ if (effort === "") {
2457
+ const next = { ...model };
2458
+ delete next.defaultEffort;
2459
+ return next;
2460
+ }
2461
+ return {
2462
+ ...model,
2463
+ defaultEffort: effort
2464
+ };
2314
2465
  });
2315
- if (hasLeft) return t("resetCreditLeft", { left });
2316
- return t("resetCreditRemainingUnknown");
2317
2466
  }
2318
- /** Accessible name of one Claude grant row: state, remaining, and expiry, not a control name. */
2319
- function resetCreditRowName(t, credit, now) {
2320
- const state = resetCreditState(credit, now);
2321
- const parts = [
2322
- t("resetCreditFull"),
2323
- t(STATE_KEYS[state]),
2324
- remainingText(t, credit),
2325
- expiryText(t, credit)
2326
- ];
2327
- if (state === "notUsable" && credit.cooldownUntil !== void 0 && credit.cooldownUntil > now) parts.push(t("resetCreditCooldownUntil", { date: new Date(credit.cooldownUntil).toLocaleString() }));
2328
- return parts.join(", ");
2467
+ /**
2468
+ * Set or clear one model row's local context-window override. `undefined`
2469
+ * deletes the field so the model follows the endpoint again; the runtime
2470
+ * caps a stored value at the advertised window.
2471
+ * @param models - the current list.
2472
+ * @param id - the row to edit; an unknown id leaves the list unchanged.
2473
+ * @param tokens - a positive safe integer, or `undefined` to clear.
2474
+ * @returns a new list; the input array is not mutated.
2475
+ */
2476
+ function setModelContext(models, id, tokens) {
2477
+ return models.map((model) => {
2478
+ if (model.id !== id) return model;
2479
+ const next = { ...model };
2480
+ if (tokens === void 0) delete next.contextOverride;
2481
+ else next.contextOverride = tokens;
2482
+ return next;
2483
+ });
2329
2484
  }
2330
- /** Claude omits an empty grant list; Codex still shows a defined, possibly empty, list. */
2331
- function showsResetCredits(provider, credits) {
2332
- if (provider === "codex") return credits !== void 0;
2333
- return (credits?.length ?? 0) > 0;
2485
+ /**
2486
+ * Parse the context input's text. Only a positive safe integer is a value;
2487
+ * an empty string clears the override.
2488
+ * @param text - what the user typed.
2489
+ * @returns the number, `undefined` for blank, or `null` for text to reject.
2490
+ */
2491
+ function parseContextInput(text) {
2492
+ const trimmed = text.trim();
2493
+ if (trimmed === "") return void 0;
2494
+ if (!/^\d+$/.test(trimmed)) return null;
2495
+ const value = Number(trimmed);
2496
+ return Number.isSafeInteger(value) && value > 0 ? value : null;
2334
2497
  }
2335
- /** The error line the settings card mounts. Claude never interpolates provider text. */
2336
- function ResetCreditsErrorLine({ provider, message: message$1, t }) {
2337
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2338
- style: subscriptionCardStyles.error,
2339
- children: resetCreditsErrorText(provider, message$1, t)
2340
- });
2498
+ /**
2499
+ * Untick one model: drop it from the list and keep its full row (display
2500
+ * name, default effort, context, ...) in the stash so ticking it again is
2501
+ * lossless, even when the cloud listing no longer carries the id.
2502
+ * @param models - the current list.
2503
+ * @param stash - the stash before this untick.
2504
+ * @param id - the row to untick; an absent id changes nothing.
2505
+ * @returns the new list and stash; neither input is mutated.
2506
+ */
2507
+ function untickModel(models, stash, id) {
2508
+ const row = models.find((model) => model.id === id);
2509
+ if (row === void 0) return {
2510
+ models: [...models],
2511
+ stash
2512
+ };
2513
+ return {
2514
+ models: removeModel(models, id),
2515
+ stash: new Map(stash).set(id, row)
2516
+ };
2341
2517
  }
2342
- /** 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 });
2518
+ /**
2519
+ * Tick one model: restore its stashed row when this dialog unticked it,
2520
+ * otherwise adopt it from the fetched candidates (which never copies the
2521
+ * vendor default effort). A model already in the list, or unknown to both
2522
+ * sources, leaves the list unchanged.
2523
+ * @param models - the current list.
2524
+ * @param stash - rows unticked earlier in this dialog.
2525
+ * @param candidates - the cloud list as fetched.
2526
+ * @param id - the row to tick.
2527
+ * @returns a new list and the stash without the restored row.
2528
+ */
2529
+ function tickModel(models, stash, candidates, id) {
2530
+ if (models.some((model) => model.id === id)) return {
2531
+ models: [...models],
2532
+ stash
2533
+ };
2534
+ const stashed = stash.get(id);
2535
+ if (stashed === void 0) return {
2536
+ models: adoptCandidates(models, candidates, [id]),
2537
+ stash
2538
+ };
2539
+ const next = new Map(stash);
2540
+ next.delete(id);
2541
+ return {
2542
+ models: [...models, stashed],
2543
+ stash: next
2544
+ };
2345
2545
  }
2346
- const summaryStyle = {
2347
- ...subscriptionCardStyles.usageMeta,
2348
- cursor: "pointer",
2349
- listStyle: "none",
2350
- justifyContent: "flex-start",
2351
- gap: 6
2352
- };
2353
2546
  /**
2354
- * The closed disclosure both providers share. Claude rows are plain text:
2355
- * no button, no `role="button"`, and `cursor: default`, including when
2356
- * `claimable` is true.
2547
+ * Tick every listed id in order. Rows already in the list stay where they
2548
+ * are; restored and adopted rows append in the given order.
2549
+ * @param models - the current list.
2550
+ * @param stash - rows unticked earlier in this dialog.
2551
+ * @param candidates - the cloud list as fetched.
2552
+ * @param ids - the ids to tick, typically the filtered view.
2553
+ * @returns the new list and stash; neither input is mutated.
2357
2554
  */
2358
- function ResetCreditsDisclosure({ mode, credits, t, now = Date.now(), nextCredit, useAction, statusMessage }) {
2359
- const count = resetCreditsAvailableCount(credits, now, mode);
2360
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
2361
- className: "subscriptions-reset-credits",
2362
- style: subscriptionCardStyles.usageRow,
2363
- children: [
2364
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
2365
- style: summaryStyle,
2366
- children: [
2367
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
2368
- "aria-hidden": "true",
2369
- width: "12",
2370
- height: "12",
2371
- viewBox: "0 0 16 16",
2372
- style: { flexShrink: 0 },
2373
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
2374
- d: "m6 3 5 5-5 5",
2375
- fill: "none",
2376
- stroke: "currentColor",
2377
- strokeWidth: "1.5",
2378
- strokeLinecap: "round",
2379
- strokeLinejoin: "round"
2380
- })
2381
- }),
2382
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("resetCreditsTitle") }),
2383
- mode === "grants" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("resetCreditReadOnlyLabel") }),
2384
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2385
- style: { marginLeft: "auto" },
2386
- children: t("resetCreditsAvailable", { count })
2387
- })
2388
- ]
2389
- }),
2390
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: ".subscriptions-reset-credits > summary::-webkit-details-marker { display: none; }.subscriptions-reset-credits[open] > summary > svg { transform: rotate(90deg); }" }),
2391
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2392
- style: {
2393
- paddingLeft: 18,
2394
- marginTop: 6
2395
- },
2396
- children: [
2397
- mode === "grants" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2398
- style: subscriptionCardStyles.status,
2399
- children: t("resetCreditReadOnly")
2400
- }),
2401
- credits.map((credit, index) => {
2402
- const state = resetCreditState(credit, now);
2403
- const showUse = mode === "credits" && useAction !== void 0 && credit === nextCredit;
2404
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2405
- "data-reset-state": mode === "grants" ? state : void 0,
2406
- "data-reset-claimable": mode === "grants" && credit.claimable === true ? "true" : void 0,
2407
- role: mode === "grants" ? "group" : void 0,
2408
- "aria-label": mode === "grants" ? resetCreditRowName(t, credit, now) : void 0,
2409
- style: {
2410
- ...subscriptionCardStyles.usageMeta,
2411
- minHeight: 28,
2412
- gap: 12,
2413
- cursor: mode === "grants" ? "default" : void 0
2414
- },
2415
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2416
- style: {
2417
- display: "inline-flex",
2418
- alignItems: "center",
2419
- gap: 8,
2420
- flexShrink: 0
2421
- },
2422
- children: [
2423
- t("resetCreditFull"),
2424
- mode === "grants" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2425
- "aria-hidden": "true",
2426
- "data-reset-state-word": state,
2427
- children: t(STATE_KEYS[state])
2428
- }),
2429
- showUse && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2430
- type: "button",
2431
- "aria-disabled": useAction.disabled,
2432
- title: useAction.title,
2433
- style: {
2434
- minHeight: 22,
2435
- height: 22,
2436
- padding: "0 7px",
2437
- borderRadius: 6,
2438
- opacity: useAction.disabled ? .35 : 1,
2439
- cursor: useAction.disabled ? "not-allowed" : "pointer"
2440
- },
2441
- onClick: () => {
2442
- if (!useAction.disabled) useAction.onUse();
2443
- },
2444
- children: useAction.busy ? t("resetUseChecking") : useAction.label
2445
- })
2446
- ]
2447
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2448
- style: { textAlign: "right" },
2449
- children: [
2450
- mode === "grants" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2451
- "aria-hidden": "true",
2452
- children: [remainingText(t, credit), " · "]
2453
- }),
2454
- mode === "grants" && state === "notUsable" && credit.cooldownUntil !== void 0 && credit.cooldownUntil > now && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2455
- "aria-hidden": "true",
2456
- children: [t("resetCreditCooldownUntil", { date: new Date(credit.cooldownUntil).toLocaleString() }), " · "]
2457
- }),
2458
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2459
- "aria-hidden": mode === "grants" ? true : void 0,
2460
- children: expiryText(t, credit)
2461
- })
2462
- ]
2463
- })]
2464
- }, credit.id ?? index);
2465
- }),
2466
- statusMessage !== void 0 && statusMessage !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2467
- role: "status",
2468
- style: subscriptionCardStyles.status,
2469
- children: statusMessage
2470
- })
2471
- ]
2472
- })
2473
- ]
2555
+ function tickModels(models, stash, candidates, ids) {
2556
+ let state = {
2557
+ models: [...models],
2558
+ stash
2559
+ };
2560
+ for (const id of ids) state = tickModel(state.models, state.stash, candidates, id);
2561
+ return state;
2562
+ }
2563
+ /**
2564
+ * Untick every listed id, stashing each removed row. Ids outside the list,
2565
+ * such as filtered-out rows, are untouched.
2566
+ * @param models - the current list.
2567
+ * @param stash - the stash before this untick.
2568
+ * @param ids - the ids to untick, typically the filtered view.
2569
+ * @returns the new list and stash; neither input is mutated.
2570
+ */
2571
+ function untickModels(models, stash, ids) {
2572
+ let state = {
2573
+ models: [...models],
2574
+ stash
2575
+ };
2576
+ for (const id of ids) state = untickModel(state.models, state.stash, id);
2577
+ return state;
2578
+ }
2579
+ /**
2580
+ * The effort choices for one ticked row. The row's own `efforts` decide the
2581
+ * ids because the store rejects a default outside them, so the selector must
2582
+ * never offer anything else. A snapshot of the endpoint (`labels`) may only
2583
+ * supply friendlier names for the same ids. A saved default the list no
2584
+ * longer contains is kept visible and marked unavailable instead of
2585
+ * silently reading as "follow the provider".
2586
+ * @param row - the draft row.
2587
+ * @param fetched - efforts from this dialog's latest fetch, when it returned the row.
2588
+ * @param labels - the opened dialog's snapshot of named efforts for the id.
2589
+ * @returns the options in picker order.
2590
+ */
2591
+ function effortOptions(row, fetched, labels) {
2592
+ const ids = row.efforts ?? fetched ?? labels?.map((entry) => entry.id) ?? [];
2593
+ const options = ids.map((id) => ({
2594
+ id,
2595
+ name: labels?.find((entry) => entry.id === id)?.name ?? id
2596
+ }));
2597
+ if (row.defaultEffort !== void 0 && !ids.includes(row.defaultEffort)) options.push({
2598
+ id: row.defaultEffort,
2599
+ name: row.defaultEffort,
2600
+ unavailable: true
2474
2601
  });
2602
+ return options;
2475
2603
  }
2476
2604
 
2477
2605
  //#endregion
2478
- //#region src/client/UsageBadgeDisplaySetting.tsx
2479
- /** Browser-local display preference; never changes provider or account settings. */
2480
- function UsageBadgeDisplaySetting({ t }) {
2481
- const mode = useUsageBadgeMode();
2606
+ //#region src/client/OllamaSettingsManager.tsx
2607
+ const border = "0.5px solid var(--dsw-alias-border-l2)";
2608
+ const stack = {
2609
+ display: "grid",
2610
+ gap: 12,
2611
+ minWidth: 0
2612
+ };
2613
+ const actions = {
2614
+ display: "flex",
2615
+ flexWrap: "wrap",
2616
+ gap: 8,
2617
+ alignItems: "center"
2618
+ };
2619
+ function message$1(error) {
2620
+ return error instanceof Error ? error.message : String(error);
2621
+ }
2622
+ /** `vision · thinking · context 262K`, dropping whatever the model does not advertise. */
2623
+ function capabilities(t, model) {
2624
+ const context = contextLabel(model.contextWindow);
2625
+ return [
2626
+ model.vision === true ? t("ollamaCapVision") : false,
2627
+ model.thinking === true ? t("ollamaCapThinking") : false,
2628
+ context === void 0 ? false : t("ollamaContext", { tokens: context })
2629
+ ].filter(Boolean).join(" · ");
2630
+ }
2631
+ /**
2632
+ * Ollama Cloud settings dialog. It owns one draft (model list, web toggle,
2633
+ * base URL and credential reference) that Save writes in a single
2634
+ * `ollamaSetSettings` call, so a failure keeps every edit. Fetching the cloud
2635
+ * catalog is a read-only side trip, and a key typed for the fetch never
2636
+ * leaves this component. The model list follows the shared model editor: a
2637
+ * filter, select all/none over the filtered rows, and one checkbox per
2638
+ * model, where ticking edits the draft and enables Save. Ollama adds what the
2639
+ * shared editor lacks: a display name, ordering, and a local context window.
2640
+ * Unticking keeps the row's full settings in a dialog-local stash, so ticking
2641
+ * it again restores them even when the cloud listing no longer returns it.
2642
+ */
2643
+ function OllamaSettingsManager({ rpc, t, onClose, onSaved }) {
2644
+ const dialog = (0, react.useRef)(null);
2645
+ const title = (0, react.useId)();
2646
+ const description = (0, react.useId)();
2647
+ const [view, setView] = (0, react.useState)();
2648
+ const [draft, setDraft] = (0, react.useState)({});
2649
+ const [dirty, setDirty] = (0, react.useState)(false);
2650
+ const [loading, setLoading] = (0, react.useState)(true);
2651
+ const [saving, setSaving] = (0, react.useState)(false);
2652
+ const [error, setError] = (0, react.useState)("");
2653
+ const [saveError, setSaveError] = (0, react.useState)("");
2654
+ const [attempt, setAttempt] = (0, react.useState)(0);
2655
+ const [apiKey, setApiKey] = (0, react.useState)("");
2656
+ const [candidates, setCandidates] = (0, react.useState)();
2657
+ const [fetching, setFetching] = (0, react.useState)(false);
2658
+ const [fetchError, setFetchError] = (0, react.useState)("");
2659
+ const [query, setQuery] = (0, react.useState)("");
2660
+ const [expanded, setExpanded] = (0, react.useState)(/* @__PURE__ */ new Set());
2661
+ const [stash, setStash] = (0, react.useState)(/* @__PURE__ */ new Map());
2662
+ const [contextText, setContextText] = (0, react.useState)({});
2663
+ const alive = (0, react.useRef)(true);
2664
+ const saveLock = (0, react.useRef)(false);
2665
+ const fetchLock = (0, react.useRef)(false);
2666
+ (0, react.useEffect)(() => {
2667
+ alive.current = true;
2668
+ const element = dialog.current;
2669
+ const previous = document.activeElement;
2670
+ element.showModal();
2671
+ return () => {
2672
+ alive.current = false;
2673
+ element.close();
2674
+ if (previous instanceof HTMLElement && previous.isConnected) previous.focus();
2675
+ };
2676
+ }, []);
2677
+ (0, react.useEffect)(() => {
2678
+ let current = true;
2679
+ setLoading(true);
2680
+ setError("");
2681
+ callSubscriptionsAuth(rpc, "ollamaSettings", {}).then((data) => {
2682
+ if (!current) return;
2683
+ setView(data);
2684
+ setDraft({ ...data.settings });
2685
+ setStash(/* @__PURE__ */ new Map());
2686
+ setContextText({});
2687
+ setDirty(false);
2688
+ }).catch((failure) => {
2689
+ if (current) setError(t("ollamaModelsLoadFailed", { message: message$1(failure) }));
2690
+ }).finally(() => {
2691
+ if (current) setLoading(false);
2692
+ });
2693
+ return () => {
2694
+ current = false;
2695
+ };
2696
+ }, [rpc, attempt]);
2697
+ function edit(next) {
2698
+ setDraft(next);
2699
+ setDirty(true);
2700
+ }
2701
+ /** Fetch the cloud catalog; a failure keeps the previous list and the draft. */
2702
+ async function fetchCandidates() {
2703
+ if (fetchLock.current) return;
2704
+ fetchLock.current = true;
2705
+ setFetching(true);
2706
+ setFetchError("");
2707
+ try {
2708
+ const value = await callSubscriptionsAuth(rpc, "ollamaModels", {
2709
+ ...apiKey ? { apiKey } : {},
2710
+ ...draft.baseURL ? { baseURL: draft.baseURL } : {}
2711
+ });
2712
+ if (!alive.current) return;
2713
+ setCandidates(value.models);
2714
+ } catch (failure) {
2715
+ if (alive.current) setFetchError(t("ollamaFetchFailed", { message: message$1(failure) }));
2716
+ } finally {
2717
+ fetchLock.current = false;
2718
+ if (alive.current) setFetching(false);
2719
+ }
2720
+ }
2721
+ /** Save the whole draft; a failure keeps the dialog open with every edit. */
2722
+ async function save() {
2723
+ if (saveLock.current) return;
2724
+ saveLock.current = true;
2725
+ setSaving(true);
2726
+ setSaveError("");
2727
+ try {
2728
+ await callSubscriptionsAuth(rpc, "ollamaSetSettings", { settings: draft });
2729
+ if (alive.current) {
2730
+ onSaved();
2731
+ onClose();
2732
+ }
2733
+ } catch (failure) {
2734
+ if (alive.current) setSaveError(t("ollamaModelsSaveFailed", { message: message$1(failure) }));
2735
+ } finally {
2736
+ saveLock.current = false;
2737
+ if (alive.current) setSaving(false);
2738
+ }
2739
+ }
2740
+ /** Drop every stored override, then reload the draft from the deployment values. */
2741
+ async function resetDefaults() {
2742
+ if (saveLock.current) return;
2743
+ if (!window.confirm(t("ollamaResetConfirm"))) return;
2744
+ saveLock.current = true;
2745
+ setSaving(true);
2746
+ setSaveError("");
2747
+ try {
2748
+ await callSubscriptionsAuth(rpc, "ollamaSetSettings", { settings: {} });
2749
+ if (alive.current) {
2750
+ setAttempt((value) => value + 1);
2751
+ onSaved();
2752
+ }
2753
+ } catch (failure) {
2754
+ if (alive.current) setSaveError(t("ollamaModelsSaveFailed", { message: message$1(failure) }));
2755
+ } finally {
2756
+ saveLock.current = false;
2757
+ if (alive.current) setSaving(false);
2758
+ }
2759
+ }
2760
+ function editOverride(field, value) {
2761
+ const next = { ...draft };
2762
+ if (value.trim()) next[field] = value;
2763
+ else delete next[field];
2764
+ edit(next);
2765
+ }
2766
+ /**
2767
+ * One list for both sources, like the shared editor: the ticked models in
2768
+ * draft order first (so Move up/down is visible), then the unticked ones
2769
+ * (stashed in this dialog, or fetched and not yet adopted). A kept model
2770
+ * the fetch did not return stays listed, ticked or not.
2771
+ */
2772
+ const fetchedById = new Map((candidates ?? []).map((candidate) => [candidate.id, candidate]));
2773
+ const present = new Set((draft.models ?? []).map((model) => model.id));
2774
+ const asRow = (model, ticked) => {
2775
+ const fetched = fetchedById.get(model.id);
2776
+ const contextWindow = fetched?.contextWindow ?? model.contextWindow;
2777
+ return {
2778
+ id: model.id,
2779
+ name: model.name ?? fetched?.name ?? model.id,
2780
+ ticked,
2781
+ vision: model.vision === true || fetched?.vision === true,
2782
+ thinking: model.thinking === true || fetched?.thinking === true,
2783
+ ...contextWindow === void 0 ? {} : { contextWindow },
2784
+ efforts: model.efforts ?? fetched?.efforts ?? []
2785
+ };
2786
+ };
2787
+ const shown = [
2788
+ ...(draft.models ?? []).map((model) => asRow(model, true)),
2789
+ ...[...stash.values()].filter((model) => !present.has(model.id)).map((model) => asRow(model, false)),
2790
+ ...(candidates ?? []).filter((candidate) => !present.has(candidate.id) && !stash.has(candidate.id)).map((candidate) => asRow({ id: candidate.id }, false))
2791
+ ].filter((row) => `${row.name} ${row.id}`.toLowerCase().includes(query.trim().toLowerCase()));
2792
+ /** Rename and default-effort edits ride the tested draft helpers. */
2793
+ function rename(id, name) {
2794
+ edit({
2795
+ ...draft,
2796
+ models: renameModel(draft.models ?? [], id, name)
2797
+ });
2798
+ }
2799
+ function setEffort(id, effort) {
2800
+ edit({
2801
+ ...draft,
2802
+ models: setModelEffort(draft.models ?? [], id, effort)
2803
+ });
2804
+ }
2805
+ /** Keep the typed text; only a valid number (or blank) reaches the draft. */
2806
+ function setContext(id, text) {
2807
+ setContextText((current) => ({
2808
+ ...current,
2809
+ [id]: text
2810
+ }));
2811
+ const tokens = parseContextInput(text);
2812
+ if (tokens !== null) edit({
2813
+ ...draft,
2814
+ models: setModelContext(draft.models ?? [], id, tokens)
2815
+ });
2816
+ }
2817
+ function applyTicks(next) {
2818
+ setStash(next.stash);
2819
+ edit({
2820
+ ...draft,
2821
+ models: next.models
2822
+ });
2823
+ }
2824
+ const invalidContext = Object.entries(contextText).filter(([id, text]) => present.has(id) && parseContextInput(text) === null).map(([id]) => id);
2825
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dialog", {
2826
+ className: "dsh-subscription-manager",
2827
+ ref: dialog,
2828
+ "aria-labelledby": title,
2829
+ "aria-describedby": description,
2830
+ onCancel: (event) => {
2831
+ if (saveLock.current) event.preventDefault();
2832
+ },
2833
+ onClose,
2834
+ style: {
2835
+ width: 620,
2836
+ maxWidth: "calc(100vw - 32px)",
2837
+ maxHeight: "calc(100dvh - 32px)",
2838
+ overflow: "auto"
2839
+ },
2840
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: providerSettingsCss }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2841
+ style: {
2842
+ ...stack,
2843
+ padding: 20
2844
+ },
2845
+ children: [
2846
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
2847
+ style: {
2848
+ ...actions,
2849
+ justifyContent: "space-between"
2850
+ },
2851
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
2852
+ id: title,
2853
+ style: { margin: 0 },
2854
+ children: t("ollamaManageTitle")
2855
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2856
+ type: "button",
2857
+ autoFocus: true,
2858
+ disabled: saving,
2859
+ onClick: onClose,
2860
+ children: t("imageClose")
2861
+ })]
2862
+ }),
2863
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2864
+ id: description,
2865
+ className: "dsh-subscription-hint",
2866
+ children: t("ollamaManageHint")
2867
+ }),
2868
+ view?.configOverridesModels === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2869
+ className: "dsh-subscription-hint",
2870
+ children: t("ollamaCatalogOverride")
2871
+ }),
2872
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2873
+ role: "alert",
2874
+ children: error
2875
+ }),
2876
+ loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2877
+ role: "status",
2878
+ className: "dsh-subscription-hint",
2879
+ children: t("modelDefaultsLoading")
2880
+ }),
2881
+ !loading && !view && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2882
+ type: "button",
2883
+ onClick: () => setAttempt((value) => value + 1),
2884
+ children: t("modelDefaultsRetry")
2885
+ }),
2886
+ view && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
2887
+ disabled: saving,
2888
+ style: {
2889
+ ...stack,
2890
+ border: 0,
2891
+ margin: 0,
2892
+ padding: 0,
2893
+ minWidth: 0
2894
+ },
2895
+ children: [
2896
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2897
+ style: {
2898
+ borderTop: border,
2899
+ paddingTop: 12,
2900
+ display: "grid",
2901
+ gap: 12
2902
+ },
2903
+ children: [
2904
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2905
+ style: { margin: 0 },
2906
+ children: t("modelsEdit")
2907
+ }),
2908
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2909
+ style: actions,
2910
+ children: [
2911
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2912
+ type: "password",
2913
+ autoComplete: "off",
2914
+ style: { flex: "1 1 180px" },
2915
+ "aria-label": t("ollamaKeyPlaceholder"),
2916
+ placeholder: t("ollamaKeyPlaceholder"),
2917
+ value: apiKey,
2918
+ onChange: (event) => setApiKey(event.target.value)
2919
+ }),
2920
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2921
+ type: "button",
2922
+ className: "dsh-subscription-quiet",
2923
+ disabled: fetching,
2924
+ onClick: () => {
2925
+ fetchCandidates();
2926
+ },
2927
+ children: fetching ? t("ollamaFetching") : t("ollamaFetch")
2928
+ }),
2929
+ candidates !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2930
+ className: "dsh-subscription-hint",
2931
+ children: t("ollamaCandidatesCount", { count: candidates.length })
2932
+ })
2933
+ ]
2934
+ }),
2935
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2936
+ className: "dsh-subscription-hint",
2937
+ children: t("ollamaKeyHint")
2938
+ }),
2939
+ fetchError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2940
+ role: "alert",
2941
+ children: fetchError
2942
+ }),
2943
+ candidates?.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2944
+ className: "dsh-subscription-hint",
2945
+ children: t("ollamaCandidatesEmpty")
2946
+ }),
2947
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2948
+ style: actions,
2949
+ children: [
2950
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2951
+ style: { flex: "1 1 180px" },
2952
+ value: query,
2953
+ onChange: (event) => setQuery(event.target.value),
2954
+ placeholder: t("modelDefaultsFilterPlaceholder"),
2955
+ "aria-label": t("modelDefaultsFilterPlaceholder")
2956
+ }),
2957
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2958
+ type: "button",
2959
+ onClick: () => applyTicks(tickModels(draft.models ?? [], stash, candidates ?? [], shown.map((row) => row.id))),
2960
+ children: t("modelsSelectAll")
2961
+ }),
2962
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2963
+ type: "button",
2964
+ onClick: () => applyTicks(untickModels(draft.models ?? [], stash, shown.map((row) => row.id))),
2965
+ children: t("modelsSelectNone")
2966
+ })
2967
+ ]
2968
+ }),
2969
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2970
+ style: {
2971
+ maxHeight: 360,
2972
+ overflowY: "auto",
2973
+ display: "grid",
2974
+ gap: 8
2975
+ },
2976
+ children: [shown.map((row) => {
2977
+ const stored = (draft.models ?? []).find((entry) => entry.id === row.id);
2978
+ const index = (draft.models ?? []).findIndex((entry) => entry.id === row.id);
2979
+ const open = expanded.has(row.id);
2980
+ const details = `${row.id}-details`;
2981
+ const efforts = stored === void 0 ? [] : effortOptions(stored, fetchedById.get(row.id)?.efforts, view.models.find((entry) => entry.id === row.id)?.efforts);
2982
+ const contextProblem = invalidContext.includes(row.id);
2983
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2984
+ style: {
2985
+ borderBottom: border,
2986
+ padding: "8px 2px",
2987
+ display: "grid",
2988
+ gap: 8
2989
+ },
2990
+ children: [
2991
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2992
+ style: { overflowWrap: "anywhere" },
2993
+ children: [
2994
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2995
+ type: "checkbox",
2996
+ checked: row.ticked,
2997
+ onChange: (event) => {
2998
+ applyTicks(event.target.checked ? tickModel(draft.models ?? [], stash, candidates ?? [], row.id) : untickModel(draft.models ?? [], stash, row.id));
2999
+ }
3000
+ }),
3001
+ " ",
3002
+ row.name,
3003
+ " ",
3004
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3005
+ style: subscriptionCardStyles.status,
3006
+ children: capabilities(t, row)
3007
+ })
3008
+ ]
3009
+ }),
3010
+ stored !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3011
+ style: actions,
3012
+ children: [
3013
+ row.thinking && efforts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3014
+ style: actions,
3015
+ children: [t("modelDefaultsTitle"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3016
+ "aria-label": `${row.name} ${t("modelDefaultsTitle")}`,
3017
+ value: stored.defaultEffort ?? "",
3018
+ onChange: (event) => setEffort(row.id, event.target.value),
3019
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3020
+ value: "",
3021
+ children: t("modelDefaultsFollowProvider")
3022
+ }), efforts.map((effort) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3023
+ value: effort.id,
3024
+ disabled: effort.unavailable === true,
3025
+ children: effort.unavailable === true ? `${effort.name} (${t("modelsUnavailable")})` : effort.name
3026
+ }, effort.id))]
3027
+ })]
3028
+ }),
3029
+ row.thinking && efforts.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("ollamaEffortsUnknown") }),
3030
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3031
+ style: actions,
3032
+ children: [t("modelsContext"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3033
+ style: { width: 140 },
3034
+ inputMode: "numeric",
3035
+ "aria-invalid": contextProblem,
3036
+ value: contextText[row.id] ?? (stored.contextOverride === void 0 ? "" : String(stored.contextOverride)),
3037
+ "aria-label": `${row.name} ${t("modelsContext")}`,
3038
+ placeholder: row.contextWindow === void 0 ? "" : String(row.contextWindow),
3039
+ onChange: (event) => setContext(row.id, event.target.value)
3040
+ })]
3041
+ }),
3042
+ row.contextWindow !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("modelsContextBounds", {
3043
+ default: row.contextWindow,
3044
+ max: row.contextWindow
3045
+ }) }),
3046
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3047
+ type: "button",
3048
+ className: "dsh-subscription-quiet",
3049
+ "aria-expanded": open,
3050
+ "aria-controls": details,
3051
+ "aria-label": `${row.name} ${t("ollamaRowDetails")}`,
3052
+ onClick: () => {
3053
+ const next = new Set(expanded);
3054
+ if (open) next.delete(row.id);
3055
+ else next.add(row.id);
3056
+ setExpanded(next);
3057
+ },
3058
+ children: [
3059
+ t("ollamaRowDetails"),
3060
+ " ",
3061
+ open ? "▾" : "▸"
3062
+ ]
3063
+ })
3064
+ ]
3065
+ }),
3066
+ contextProblem && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3067
+ role: "alert",
3068
+ children: t("modelsContextInvalid", { model: row.name })
3069
+ }),
3070
+ stored !== void 0 && open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3071
+ id: details,
3072
+ style: actions,
3073
+ children: [
3074
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3075
+ style: actions,
3076
+ children: [t("ollamaDisplayName"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3077
+ value: stored.name ?? "",
3078
+ placeholder: row.id,
3079
+ "aria-label": `${row.name} ${t("ollamaDisplayName")}`,
3080
+ onChange: (event) => rename(row.id, event.target.value),
3081
+ onBlur: (event) => rename(row.id, event.target.value.trim())
3082
+ })]
3083
+ }),
3084
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3085
+ type: "button",
3086
+ disabled: index === 0,
3087
+ "aria-label": `${row.name} ${t("ollamaMoveUp")}`,
3088
+ onClick: () => edit({
3089
+ ...draft,
3090
+ models: moveModel(draft.models ?? [], row.id, -1)
3091
+ }),
3092
+ children: t("ollamaMoveUp")
3093
+ }),
3094
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3095
+ type: "button",
3096
+ disabled: index === (draft.models ?? []).length - 1,
3097
+ "aria-label": `${row.name} ${t("ollamaMoveDown")}`,
3098
+ onClick: () => edit({
3099
+ ...draft,
3100
+ models: moveModel(draft.models ?? [], row.id, 1)
3101
+ }),
3102
+ children: t("ollamaMoveDown")
3103
+ })
3104
+ ]
3105
+ })
3106
+ ]
3107
+ }, row.id);
3108
+ }), shown.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3109
+ className: "dsh-subscription-hint",
3110
+ children: query.trim() ? t("modelDefaultsFilterEmpty", { query }) : t("ollamaModelsEmpty")
3111
+ })]
3112
+ }),
3113
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("modelsContextHint") })
3114
+ ]
3115
+ }),
3116
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3117
+ style: {
3118
+ ...stack,
3119
+ gap: 6
3120
+ },
3121
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3122
+ className: "dsh-subscription-label",
3123
+ children: t("ollamaBaseURL")
3124
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3125
+ value: draft.baseURL ?? "",
3126
+ placeholder: view.effective.baseURL,
3127
+ onChange: (event) => editOverride("baseURL", event.target.value)
3128
+ })]
3129
+ }),
3130
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3131
+ style: {
3132
+ ...stack,
3133
+ gap: 6
3134
+ },
3135
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3136
+ className: "dsh-subscription-label",
3137
+ children: t("ollamaApiKeyRef")
3138
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3139
+ value: draft.apiKeyRef ?? "",
3140
+ placeholder: view.effective.apiKeyRef,
3141
+ onChange: (event) => editOverride("apiKeyRef", event.target.value)
3142
+ })]
3143
+ }),
3144
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3145
+ className: "dsh-subscription-hint",
3146
+ children: t("ollamaConnectionHint")
3147
+ }),
3148
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
3149
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3150
+ type: "checkbox",
3151
+ checked: (draft.web ?? view.effective.web) === true,
3152
+ onChange: (event) => edit({
3153
+ ...draft,
3154
+ web: event.target.checked
3155
+ })
3156
+ }),
3157
+ " ",
3158
+ t("modelsWebSearch")
3159
+ ] }),
3160
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3161
+ className: "dsh-subscription-hint",
3162
+ children: t("ollamaWebHint")
3163
+ })
3164
+ ]
3165
+ }),
3166
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
3167
+ style: {
3168
+ ...stack,
3169
+ gap: 8,
3170
+ borderTop: border,
3171
+ padding: "14px 0 8px",
3172
+ position: "sticky",
3173
+ bottom: 0,
3174
+ background: "var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1))"
3175
+ },
3176
+ children: [saveError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3177
+ role: "alert",
3178
+ children: saveError
3179
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3180
+ style: {
3181
+ ...actions,
3182
+ justifyContent: "flex-end"
3183
+ },
3184
+ children: [
3185
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3186
+ type: "button",
3187
+ style: { marginRight: "auto" },
3188
+ disabled: loading || saving,
3189
+ onClick: () => {
3190
+ resetDefaults();
3191
+ },
3192
+ children: t("ollamaResetDefaults")
3193
+ }),
3194
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3195
+ type: "button",
3196
+ disabled: saving,
3197
+ onClick: onClose,
3198
+ children: t("cancel")
3199
+ }),
3200
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3201
+ type: "button",
3202
+ className: "dsh-subscription-primary",
3203
+ disabled: loading || saving || !dirty || invalidContext.length > 0,
3204
+ onClick: () => {
3205
+ save();
3206
+ },
3207
+ children: saving ? t("modelDefaultsSaving") : t("modelsSave")
3208
+ })
3209
+ ]
3210
+ })]
3211
+ })
3212
+ ]
3213
+ })]
3214
+ });
3215
+ }
3216
+
3217
+ //#endregion
3218
+ //#region src/client/OllamaCard.tsx
3219
+ function message(error) {
3220
+ return error instanceof Error ? error.message : String(error);
3221
+ }
3222
+ /**
3223
+ * Ollama Cloud route card: the catalog the model picker shows, the web
3224
+ * capability toggle, and the API-key usage row. The settings dialog owns
3225
+ * every write; the card only reads, so a save is reflected by re-reading.
3226
+ */
3227
+ function OllamaCard({ rpc, t }) {
3228
+ const [view, setView] = (0, react.useState)();
3229
+ const [configured, setConfigured] = (0, react.useState)();
3230
+ const [usage, setUsage] = (0, react.useState)();
3231
+ const [observedAt, setObservedAt] = (0, react.useState)();
3232
+ const [usageStale, setUsageStale] = (0, react.useState)(false);
3233
+ const [loading, setLoading] = (0, react.useState)(false);
3234
+ const [statusError, setStatusError] = (0, react.useState)();
3235
+ const [usageError, setUsageError] = (0, react.useState)();
3236
+ const [manage, setManage] = (0, react.useState)(false);
3237
+ const refreshUsage = (0, react.useCallback)(async () => {
3238
+ setLoading(true);
3239
+ try {
3240
+ const value = await callSubscriptionsAuth(rpc, "externalUsage", { source: "ollama-cloud" });
3241
+ setUsage(value);
3242
+ setObservedAt(value.observedAt ?? Date.now());
3243
+ setUsageStale(value.stale === true);
3244
+ setUsageError(void 0);
3245
+ window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
3246
+ } catch (failure) {
3247
+ setUsageError(message(failure));
3248
+ setUsageStale(true);
3249
+ } finally {
3250
+ setLoading(false);
3251
+ }
3252
+ }, [rpc]);
3253
+ const refreshStatus = (0, react.useCallback)(async () => {
3254
+ try {
3255
+ const [settings, status] = await Promise.all([callSubscriptionsAuth(rpc, "ollamaSettings", {}), callSubscriptionsAuth(rpc, "externalStatus", {})]);
3256
+ setView(settings);
3257
+ setStatusError(void 0);
3258
+ const isConfigured = status["ollama-cloud"]?.configured === true;
3259
+ setConfigured(isConfigured);
3260
+ if (isConfigured) refreshUsage();
3261
+ } catch (failure) {
3262
+ setStatusError(message(failure));
3263
+ }
3264
+ }, [rpc, refreshUsage]);
3265
+ (0, react.useEffect)(() => {
3266
+ refreshStatus();
3267
+ }, [refreshStatus]);
3268
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3269
+ style: subscriptionCardStyles.card,
3270
+ children: [
3271
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3272
+ style: subscriptionCardStyles.header,
3273
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
3274
+ ...subscriptionCardStyles.dot,
3275
+ background: configured === true ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-label-dimmed)"
3276
+ } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3277
+ style: subscriptionCardStyles.name,
3278
+ children: t("ollamaTitle")
3279
+ })]
3280
+ }),
3281
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3282
+ style: subscriptionCardStyles.status,
3283
+ children: view === void 0 ? t("checking") : view.models.length > 0 ? t("ollamaStatus", { count: view.models.length }) : t("ollamaStatusNone")
3284
+ }),
3285
+ view !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3286
+ style: subscriptionCardStyles.status,
3287
+ children: view.effective.web ? t("ollamaWebOn") : t("ollamaWebOff")
3288
+ }),
3289
+ statusError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3290
+ style: subscriptionCardStyles.error,
3291
+ children: t("ollamaModelsLoadFailed", { message: statusError })
3292
+ }),
3293
+ view !== void 0 && configured === false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3294
+ style: subscriptionCardStyles.status,
3295
+ children: t("ollamaConfigureHint", { ref: view.effective.apiKeyRef })
3296
+ }),
3297
+ configured === true && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3298
+ style: subscriptionCardStyles.account,
3299
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3300
+ style: subscriptionCardStyles.accountHeader,
3301
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3302
+ style: subscriptionCardStyles.defaultStar,
3303
+ title: t("defaultBadge"),
3304
+ children: "★"
3305
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3306
+ style: subscriptionCardStyles.accountName,
3307
+ children: t("externalUsageApiKey")
3308
+ })]
3309
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3310
+ style: subscriptionCardStyles.usage,
3311
+ children: [
3312
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3313
+ style: subscriptionCardStyles.usageHeader,
3314
+ children: [
3315
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3316
+ style: subscriptionCardStyles.usageTitle,
3317
+ children: t("usageTitle")
3318
+ }),
3319
+ usage?.plan !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3320
+ style: subscriptionCardStyles.usagePlan,
3321
+ children: t("usagePlan", { plan: usage.plan })
3322
+ }),
3323
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3324
+ type: "button",
3325
+ style: subscriptionCardStyles.usageRefresh,
3326
+ disabled: loading,
3327
+ onClick: () => {
3328
+ refreshUsage();
3329
+ },
3330
+ children: t("usageRefresh")
3331
+ })
3332
+ ]
3333
+ }),
3334
+ loading && usage === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3335
+ style: subscriptionCardStyles.status,
3336
+ children: t("usageLoading")
3337
+ }),
3338
+ usageError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3339
+ style: subscriptionCardStyles.error,
3340
+ children: t("usageError", { message: usageError })
3341
+ }),
3342
+ usage !== void 0 && usage.supported === false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3343
+ style: subscriptionCardStyles.status,
3344
+ children: t("usageUnsupported")
3345
+ }),
3346
+ usage?.windows?.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3347
+ style: subscriptionCardStyles.status,
3348
+ children: t("usageEmpty")
3349
+ }),
3350
+ usage?.windows?.map((window$1, index) => {
3351
+ const percent = displayUsedPercent(window$1.usedPercent);
3352
+ const label = externalUsageWindowLabel(t, window$1);
3353
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3354
+ style: subscriptionCardStyles.usageRow,
3355
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3356
+ style: subscriptionCardStyles.usageMeta,
3357
+ 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() })}`] })]
3358
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageMeter, {
3359
+ window: window$1,
3360
+ t,
3361
+ observedAt,
3362
+ stale: usageStale
3363
+ })]
3364
+ }, index);
3365
+ })
3366
+ ]
3367
+ })]
3368
+ }),
3369
+ view?.configOverridesModels === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3370
+ style: subscriptionCardStyles.status,
3371
+ children: t("ollamaCatalogOverride")
3372
+ }),
3373
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3374
+ style: subscriptionCardStyles.actions,
3375
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3376
+ type: "button",
3377
+ style: subscriptionCardStyles.button,
3378
+ "aria-haspopup": "dialog",
3379
+ onClick: () => setManage(true),
3380
+ children: t("accountsManage")
3381
+ }), configured !== true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3382
+ type: "button",
3383
+ style: subscriptionCardStyles.button,
3384
+ onClick: () => {
3385
+ refreshStatus();
3386
+ },
3387
+ children: t("usageRefresh")
3388
+ })]
3389
+ }),
3390
+ manage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OllamaSettingsManager, {
3391
+ rpc,
3392
+ t,
3393
+ onClose: () => setManage(false),
3394
+ onSaved: () => {
3395
+ refreshStatus();
3396
+ }
3397
+ })
3398
+ ]
3399
+ });
3400
+ }
3401
+
3402
+ //#endregion
3403
+ //#region src/client/reset-credits-view.tsx
3404
+ const STATE_KEYS = {
3405
+ available: "resetCreditAvailable",
3406
+ notUsable: "resetCreditNotUsable",
3407
+ paused: "resetCreditPaused",
3408
+ exhausted: "resetCreditExhausted",
3409
+ expired: "resetCreditExpired"
3410
+ };
3411
+ /**
3412
+ * Classify one grant. Expired outranks every other flag. A future
3413
+ * `cooldownUntil` or `usableNow: false` is not usable even when `claimable`
3414
+ * is true. `claimable` does not change the word: it is not an action.
3415
+ */
3416
+ function resetCreditState(credit, now) {
3417
+ if (credit.expiresAt !== void 0 && credit.expiresAt <= now) return "expired";
3418
+ if (credit.paused === true) return "paused";
3419
+ if (credit.resetsLeft === 0) return "exhausted";
3420
+ if (credit.cooldownUntil !== void 0 && credit.cooldownUntil > now || credit.usableNow === false) return "notUsable";
3421
+ return "available";
3422
+ }
3423
+ /**
3424
+ * Headline count. Codex passes one entry per credit, so the count is the
3425
+ * list length. Claude passes one entry per grant, so the count is the sum of
3426
+ * `resetsLeft` on grants that are neither expired nor paused. A missing
3427
+ * `resetsLeft` adds nothing; the row says the count is unavailable.
3428
+ */
3429
+ function resetCreditsAvailableCount(credits, now, mode) {
3430
+ if (mode === "credits") return credits.length;
3431
+ let sum = 0;
3432
+ for (const credit of credits) {
3433
+ const state = resetCreditState(credit, now);
3434
+ if (state === "expired" || state === "paused") continue;
3435
+ if (typeof credit.resetsLeft === "number" && Number.isFinite(credit.resetsLeft) && credit.resetsLeft > 0) sum += credit.resetsLeft;
3436
+ }
3437
+ return sum;
3438
+ }
3439
+ function expiryText(t, credit) {
3440
+ return credit.expiresAt === void 0 ? t("resetCreditExpiryUnknown") : t("resetCreditExpires", { date: new Date(credit.expiresAt).toLocaleString() });
3441
+ }
3442
+ function remainingText(t, credit) {
3443
+ const left = credit.resetsLeft;
3444
+ const total = credit.resetsTotal;
3445
+ const hasLeft = typeof left === "number" && Number.isFinite(left);
3446
+ if (hasLeft && typeof total === "number" && Number.isFinite(total)) return t("resetCreditRemaining", {
3447
+ left,
3448
+ total
3449
+ });
3450
+ if (hasLeft) return t("resetCreditLeft", { left });
3451
+ return t("resetCreditRemainingUnknown");
3452
+ }
3453
+ /** Accessible name of one Claude grant row: state, remaining, and expiry, not a control name. */
3454
+ function resetCreditRowName(t, credit, now) {
3455
+ const state = resetCreditState(credit, now);
3456
+ const parts = [
3457
+ t("resetCreditFull"),
3458
+ t(STATE_KEYS[state]),
3459
+ remainingText(t, credit),
3460
+ expiryText(t, credit)
3461
+ ];
3462
+ if (state === "notUsable" && credit.cooldownUntil !== void 0 && credit.cooldownUntil > now) parts.push(t("resetCreditCooldownUntil", { date: new Date(credit.cooldownUntil).toLocaleString() }));
3463
+ return parts.join(", ");
3464
+ }
3465
+ /** Claude omits an empty grant list; Codex still shows a defined, possibly empty, list. */
3466
+ function showsResetCredits(provider, credits) {
3467
+ if (provider === "codex") return credits !== void 0;
3468
+ return (credits?.length ?? 0) > 0;
3469
+ }
3470
+ /** The error line the settings card mounts. Claude never interpolates provider text. */
3471
+ function ResetCreditsErrorLine({ provider, message: message$3, t }) {
3472
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3473
+ style: subscriptionCardStyles.error,
3474
+ children: resetCreditsErrorText(provider, message$3, t)
3475
+ });
3476
+ }
3477
+ /** Fixed sentence for Claude. Codex may include the lookup message; Claude must not. */
3478
+ function resetCreditsErrorText(provider, message$3, t) {
3479
+ return provider === "claude" ? t("resetCreditsErrorFixed") : t("resetCreditsError", { message: message$3 });
3480
+ }
3481
+ const summaryStyle = {
3482
+ ...subscriptionCardStyles.usageMeta,
3483
+ cursor: "pointer",
3484
+ listStyle: "none",
3485
+ justifyContent: "flex-start",
3486
+ gap: 6
3487
+ };
3488
+ /**
3489
+ * The closed disclosure both providers share. Claude rows are plain text:
3490
+ * no button, no `role="button"`, and `cursor: default`, including when
3491
+ * `claimable` is true.
3492
+ */
3493
+ function ResetCreditsDisclosure({ mode, credits, t, now = Date.now(), nextCredit, useAction, statusMessage }) {
3494
+ const count = resetCreditsAvailableCount(credits, now, mode);
3495
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
3496
+ className: "subscriptions-reset-credits",
3497
+ style: subscriptionCardStyles.usageRow,
3498
+ children: [
3499
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
3500
+ style: summaryStyle,
3501
+ children: [
3502
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
3503
+ "aria-hidden": "true",
3504
+ width: "12",
3505
+ height: "12",
3506
+ viewBox: "0 0 16 16",
3507
+ style: { flexShrink: 0 },
3508
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
3509
+ d: "m6 3 5 5-5 5",
3510
+ fill: "none",
3511
+ stroke: "currentColor",
3512
+ strokeWidth: "1.5",
3513
+ strokeLinecap: "round",
3514
+ strokeLinejoin: "round"
3515
+ })
3516
+ }),
3517
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("resetCreditsTitle") }),
3518
+ mode === "grants" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("resetCreditReadOnlyLabel") }),
3519
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3520
+ style: { marginLeft: "auto" },
3521
+ children: t("resetCreditsAvailable", { count })
3522
+ })
3523
+ ]
3524
+ }),
3525
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: ".subscriptions-reset-credits > summary::-webkit-details-marker { display: none; }.subscriptions-reset-credits[open] > summary > svg { transform: rotate(90deg); }" }),
3526
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3527
+ style: {
3528
+ paddingLeft: 18,
3529
+ marginTop: 6
3530
+ },
3531
+ children: [
3532
+ mode === "grants" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3533
+ style: subscriptionCardStyles.status,
3534
+ children: t("resetCreditReadOnly")
3535
+ }),
3536
+ credits.map((credit, index) => {
3537
+ const state = resetCreditState(credit, now);
3538
+ const showUse = mode === "credits" && useAction !== void 0 && credit === nextCredit;
3539
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3540
+ "data-reset-state": mode === "grants" ? state : void 0,
3541
+ "data-reset-claimable": mode === "grants" && credit.claimable === true ? "true" : void 0,
3542
+ role: mode === "grants" ? "group" : void 0,
3543
+ "aria-label": mode === "grants" ? resetCreditRowName(t, credit, now) : void 0,
3544
+ style: {
3545
+ ...subscriptionCardStyles.usageMeta,
3546
+ minHeight: 28,
3547
+ gap: 12,
3548
+ cursor: mode === "grants" ? "default" : void 0
3549
+ },
3550
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3551
+ style: {
3552
+ display: "inline-flex",
3553
+ alignItems: "center",
3554
+ gap: 8,
3555
+ flexShrink: 0
3556
+ },
3557
+ children: [
3558
+ t("resetCreditFull"),
3559
+ mode === "grants" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3560
+ "aria-hidden": "true",
3561
+ "data-reset-state-word": state,
3562
+ children: t(STATE_KEYS[state])
3563
+ }),
3564
+ showUse && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3565
+ type: "button",
3566
+ "aria-disabled": useAction.disabled,
3567
+ title: useAction.title,
3568
+ style: {
3569
+ minHeight: 22,
3570
+ height: 22,
3571
+ padding: "0 7px",
3572
+ borderRadius: 6,
3573
+ opacity: useAction.disabled ? .35 : 1,
3574
+ cursor: useAction.disabled ? "not-allowed" : "pointer"
3575
+ },
3576
+ onClick: () => {
3577
+ if (!useAction.disabled) useAction.onUse();
3578
+ },
3579
+ children: useAction.busy ? t("resetUseChecking") : useAction.label
3580
+ })
3581
+ ]
3582
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3583
+ style: { textAlign: "right" },
3584
+ children: [
3585
+ mode === "grants" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3586
+ "aria-hidden": "true",
3587
+ children: [remainingText(t, credit), " · "]
3588
+ }),
3589
+ mode === "grants" && state === "notUsable" && credit.cooldownUntil !== void 0 && credit.cooldownUntil > now && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3590
+ "aria-hidden": "true",
3591
+ children: [t("resetCreditCooldownUntil", { date: new Date(credit.cooldownUntil).toLocaleString() }), " · "]
3592
+ }),
3593
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3594
+ "aria-hidden": mode === "grants" ? true : void 0,
3595
+ children: expiryText(t, credit)
3596
+ })
3597
+ ]
3598
+ })]
3599
+ }, credit.id ?? index);
3600
+ }),
3601
+ statusMessage !== void 0 && statusMessage !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3602
+ role: "status",
3603
+ style: subscriptionCardStyles.status,
3604
+ children: statusMessage
3605
+ })
3606
+ ]
3607
+ })
3608
+ ]
3609
+ });
3610
+ }
3611
+
3612
+ //#endregion
3613
+ //#region src/client/UsageBadgeDisplaySetting.tsx
3614
+ /** Browser-local display preference; never changes provider or account settings. */
3615
+ function UsageBadgeDisplaySetting({ t }) {
3616
+ const mode = useUsageBadgeMode();
2482
3617
  const preset = useUsageColorPreset();
2483
3618
  const [failed, setFailed] = (0, react.useState)(false);
2484
3619
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2872,12 +4007,12 @@ function SubscriptionsSection(props) {
2872
4007
  const usageInflightRef = (0, react.useRef)(/* @__PURE__ */ new Map());
2873
4008
  const usageRosterSignatureRef = (0, react.useRef)();
2874
4009
  const [managedProvider, setManagedProvider] = (0, react.useState)();
2875
- const setProviderError = (0, react.useCallback)((provider, message$1) => {
4010
+ const setProviderError = (0, react.useCallback)((provider, message$3) => {
2876
4011
  if (!mountedRef.current) return;
2877
4012
  setErrors((prev) => {
2878
4013
  const next = { ...prev };
2879
- if (message$1 === void 0) delete next[provider];
2880
- else next[provider] = message$1;
4014
+ if (message$3 === void 0) delete next[provider];
4015
+ else next[provider] = message$3;
2881
4016
  return next;
2882
4017
  });
2883
4018
  }, []);
@@ -2895,8 +4030,8 @@ function SubscriptionsSection(props) {
2895
4030
  try {
2896
4031
  response = await callSubscriptionsAuth(rpc, "status", {});
2897
4032
  } catch (error) {
2898
- const message$1 = error instanceof Error ? error.message : String(error);
2899
- for (const { id } of PROVIDERS) setProviderError(id, message$1);
4033
+ const message$3 = error instanceof Error ? error.message : String(error);
4034
+ for (const { id } of PROVIDERS) setProviderError(id, message$3);
2900
4035
  return;
2901
4036
  }
2902
4037
  if (!mountedRef.current) return;
@@ -3466,6 +4601,10 @@ function SubscriptionsSection(props) {
3466
4601
  rpc,
3467
4602
  t
3468
4603
  }),
4604
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OllamaCard, {
4605
+ rpc,
4606
+ t
4607
+ }),
3469
4608
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExternalUsageCards, {
3470
4609
  rpc,
3471
4610
  t
@@ -4573,7 +5712,8 @@ const PROVIDER_NAMES = {
4573
5712
  "opencode-go": "OpenCode Go",
4574
5713
  "kimi-coding": "Kimi Code",
4575
5714
  "minimax": "MiniMax",
4576
- "minimax-cn": "MiniMax CN"
5715
+ "minimax-cn": "MiniMax CN",
5716
+ "ollama-cloud": "Ollama Cloud"
4577
5717
  };
4578
5718
  /** Key of one account's cached windows: `provider:accountKey`. */
4579
5719
  function usageKeyOf(provider, account) {
@@ -4649,7 +5789,8 @@ async function loadBadgeRoster(rpc) {
4649
5789
  ["opencode-go", "opencode-go"],
4650
5790
  ["kimi-code", "kimi-coding"],
4651
5791
  ["minimax", "minimax"],
4652
- ["minimax-cn", "minimax-cn"]
5792
+ ["minimax-cn", "minimax-cn"],
5793
+ ["ollama-cloud", "ollama-cloud"]
4653
5794
  ]) {
4654
5795
  refreshed.add(provider);
4655
5796
  if (external.value[source]?.configured) roster.push({
@@ -4667,7 +5808,7 @@ async function loadBadgeRoster(rpc) {
4667
5808
  }
4668
5809
  async function usageOf(rpc, { provider, account }) {
4669
5810
  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 });
5811
+ 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
5812
  return callSubscriptionsAuth(rpc, "usage", {
4672
5813
  provider,
4673
5814
  account: account.key