dsh-usage-state 0.4.4 → 0.4.6

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.
package/lib/client.js CHANGED
@@ -244,7 +244,7 @@ window.__ModuleLoader__.load({
244
244
  },
245
245
  {
246
246
  sourceId: "kimi",
247
- pattern: /(^|\.)(api\.kimi\.com|api\.moonshot\.cn|moonshot\.cn)$/
247
+ pattern: /(^|\.)(api\.kimi\.(com|ai)|api\.moonshot\.(cn|ai)|moonshot\.cn)$/
248
248
  },
249
249
  {
250
250
  sourceId: "opencode",
@@ -285,6 +285,16 @@ window.__ModuleLoader__.load({
285
285
  return;
286
286
  }
287
287
  }
288
+ /** Kimi Code and Moonshot use distinct keys and billing endpoints. */
289
+ function kimiMode(provider, endpoint) {
290
+ const origin = originOf(endpoint);
291
+ if (origin !== void 0) {
292
+ const host = new URL(origin).hostname;
293
+ if (host === "api.kimi.com" || host === "api.kimi.ai") return "coding-plan";
294
+ if (host === "api.moonshot.cn" || host === "api.moonshot.ai") return "api";
295
+ }
296
+ return /(?:^|[^a-z])kimi[-_ ]?(?:for[-_ ]?coding|coding|code)(?:$|[^a-z])/i.test(provider) ? "coding-plan" : "api";
297
+ }
288
298
  function withTarget(resolution) {
289
299
  if (resolution.sourceId === null || resolution.mode === null) return resolution;
290
300
  return {
@@ -299,13 +309,17 @@ window.__ModuleLoader__.load({
299
309
  */
300
310
  function resolveProvider(input) {
301
311
  const entry = input.config.providers[input.provider];
312
+ const sourceId = entry?.sourceId ?? suggestSourceId(input.provider, input.endpointHint) ?? null;
313
+ const defaults = sourceId === null ? void 0 : input.config.sources[sourceId];
302
314
  const declared = originOf(input.endpointHint);
303
- const baseUrl = entry?.baseUrl ?? declared;
315
+ const explicitBaseUrl = entry?.baseUrl ?? defaults?.baseUrl;
316
+ const baseUrl = explicitBaseUrl ?? declared;
317
+ const apiKeyRef = entry?.apiKeyRef ?? defaults?.apiKeyRef;
304
318
  const base = {
305
319
  provider: input.provider,
306
320
  ...baseUrl === void 0 ? {} : { baseUrl },
307
- ...entry?.baseUrl === void 0 ? {} : { baseUrlPinned: true },
308
- ...entry?.apiKeyRef === void 0 ? {} : { apiKeyRef: entry.apiKeyRef }
321
+ ...explicitBaseUrl === void 0 ? {} : { baseUrlPinned: true },
322
+ ...apiKeyRef === void 0 ? {} : { apiKeyRef }
309
323
  };
310
324
  if (entry?.mode === "hidden") return {
311
325
  ...base,
@@ -313,7 +327,6 @@ window.__ModuleLoader__.load({
313
327
  mode: null,
314
328
  reason: "hidden"
315
329
  };
316
- const sourceId = entry?.sourceId ?? suggestSourceId(input.provider, input.endpointHint) ?? null;
317
330
  if (sourceId === null) return {
318
331
  ...base,
319
332
  sourceId: null,
@@ -347,8 +360,8 @@ window.__ModuleLoader__.load({
347
360
  mode: null,
348
361
  reason: "needs-endpoint"
349
362
  };
350
- const mode = source.modes[0];
351
- if (mode === void 0) return {
363
+ const mode = sourceId === "kimi" ? kimiMode(input.provider, baseUrl) : source.modes[0];
364
+ if (mode === void 0 || !source.modes.includes(mode)) return {
352
365
  ...base,
353
366
  sourceId,
354
367
  mode: null,
@@ -550,6 +563,7 @@ window.__ModuleLoader__.load({
550
563
  const [editing, setEditing] = (0, react.useState)(false);
551
564
  const shown = editing ? draft : props.value;
552
565
  const commit = () => {
566
+ if (!editing || props.disabled) return;
553
567
  setEditing(false);
554
568
  const next = draft.trim();
555
569
  if (next !== props.value.trim()) props.onCommit(next);
@@ -579,29 +593,54 @@ window.__ModuleLoader__.load({
579
593
  const { t } = props;
580
594
  const [draft, setDraft] = (0, react.useState)("");
581
595
  const [note, setNote] = (0, react.useState)(void 0);
596
+ const [pending, setPending] = (0, react.useState)(false);
597
+ const busy = (0, react.useRef)(false);
582
598
  const candidates = props.description?.candidates ?? [];
583
- const configuredRef = props.description?.ref ?? candidates.find((candidate) => candidate.configured)?.ref ?? props.refs[0];
584
- const writable = props.description?.writable !== false;
599
+ const configuredRef = props.refOverride ?? props.description?.ref ?? candidates.find((candidate) => candidate.configured)?.ref ?? props.refs[0];
600
+ const candidate = candidates.find((value) => value.ref === configuredRef);
601
+ const sameDescription = props.description?.ref === configuredRef;
602
+ const writable = (candidate?.writable ?? (sameDescription ? props.description?.writable : void 0)) !== false;
603
+ const configured = candidate?.configured ?? (sameDescription && props.description?.configured === true);
585
604
  const save = async () => {
586
- if (props.credentials === void 0 || configuredRef === void 0 || draft.trim() === "") return;
587
- const result = await props.credentials.set(configuredRef, draft.trim());
588
- if (!result.ok) {
589
- setNote(t("credentialFailed", { message: result.error.message }));
590
- return;
605
+ if (busy.current || !writable || props.credentials === void 0 || configuredRef === void 0 || draft.trim() === "") return;
606
+ busy.current = true;
607
+ setPending(true);
608
+ setNote(void 0);
609
+ try {
610
+ const result = await props.credentials.set(configuredRef, draft.trim());
611
+ if (!result.ok) {
612
+ setNote(t("credentialFailed", { message: result.error.message }));
613
+ return;
614
+ }
615
+ setDraft("");
616
+ setNote(t("credentialSaved"));
617
+ props.onChanged();
618
+ } catch (error) {
619
+ setNote(t("credentialFailed", { message: error instanceof Error ? error.message : String(error) }));
620
+ } finally {
621
+ busy.current = false;
622
+ setPending(false);
591
623
  }
592
- setDraft("");
593
- setNote(t("credentialSaved"));
594
- props.onChanged();
595
624
  };
596
625
  const clear = async () => {
597
- if (props.credentials === void 0 || configuredRef === void 0) return;
598
- const result = await props.credentials.unset(configuredRef);
599
- if (!result.ok) {
600
- setNote(t("credentialFailed", { message: result.error.message }));
601
- return;
626
+ if (busy.current || !writable || props.credentials === void 0 || configuredRef === void 0) return;
627
+ busy.current = true;
628
+ setPending(true);
629
+ setNote(void 0);
630
+ try {
631
+ const result = await props.credentials.unset(configuredRef);
632
+ if (!result.ok) {
633
+ setNote(t("credentialFailed", { message: result.error.message }));
634
+ return;
635
+ }
636
+ setNote(t("credentialSaved"));
637
+ props.onChanged();
638
+ } catch (error) {
639
+ setNote(t("credentialFailed", { message: error instanceof Error ? error.message : String(error) }));
640
+ } finally {
641
+ busy.current = false;
642
+ setPending(false);
602
643
  }
603
- setNote(t("credentialSaved"));
604
- props.onChanged();
605
644
  };
606
645
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
607
646
  style: {
@@ -646,7 +685,7 @@ window.__ModuleLoader__.load({
646
685
  type: "password",
647
686
  autoComplete: "off",
648
687
  value: draft,
649
- disabled: !writable || props.credentials === void 0,
688
+ disabled: pending || !writable || props.credentials === void 0,
650
689
  placeholder: t("credentialPlaceholder"),
651
690
  onChange: (event) => setDraft(event.target.value),
652
691
  style: { maxWidth: "280px" }
@@ -654,14 +693,14 @@ window.__ModuleLoader__.load({
654
693
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
655
694
  size: "sm",
656
695
  variant: "primary",
657
- disabled: draft.trim() === "",
696
+ disabled: pending || !writable || props.credentials === void 0 || configuredRef === void 0 || draft.trim() === "",
658
697
  onClick: () => void save(),
659
698
  children: t("credentialSave")
660
699
  }),
661
700
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
662
701
  size: "sm",
663
702
  variant: "outline",
664
- disabled: configuredRef === void 0 || props.description?.configured !== true,
703
+ disabled: pending || !writable || props.credentials === void 0 || configuredRef === void 0 || !configured,
665
704
  onClick: () => void clear(),
666
705
  children: t("credentialClear")
667
706
  })
@@ -717,7 +756,7 @@ window.__ModuleLoader__.load({
717
756
  size: "sm",
718
757
  variant: "ghost",
719
758
  "aria-label": t("moveUp"),
720
- disabled: props.index === 0,
759
+ disabled: props.disabled || props.index === 0,
721
760
  onClick: () => props.onMove(-1),
722
761
  children: "↑"
723
762
  }),
@@ -725,13 +764,14 @@ window.__ModuleLoader__.load({
725
764
  size: "sm",
726
765
  variant: "ghost",
727
766
  "aria-label": t("moveDown"),
728
- disabled: props.index === props.total - 1,
767
+ disabled: props.disabled || props.index === props.total - 1,
729
768
  onClick: () => props.onMove(1),
730
769
  children: "↓"
731
770
  }),
732
771
  row.modes.map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
733
772
  size: "sm",
734
773
  variant: row.selected === mode ? "primary" : "outline",
774
+ disabled: props.disabled,
735
775
  onClick: () => props.onMode(mode),
736
776
  children: modeLabel(mode, t)
737
777
  }, mode))
@@ -774,6 +814,7 @@ window.__ModuleLoader__.load({
774
814
  style: MUTED,
775
815
  children: t("sourceLabel")
776
816
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
817
+ disabled: props.disabled,
777
818
  value: entry?.sourceId ?? "",
778
819
  onChange: (event) => {
779
820
  const value = event.target.value;
@@ -805,6 +846,7 @@ window.__ModuleLoader__.load({
805
846
  children: t("baseUrl")
806
847
  }),
807
848
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DraftInput, {
849
+ disabled: props.disabled,
808
850
  value: entry?.baseUrl ?? "",
809
851
  placeholder: row.resolution.mode === null ? t("baseUrlPlaceholder") : resolvedSource?.defaultBaseUrl[row.resolution.mode] ?? t("baseUrlPlaceholder"),
810
852
  width: "320px",
@@ -835,6 +877,7 @@ window.__ModuleLoader__.load({
835
877
  children: t("apiKeyRef")
836
878
  }),
837
879
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DraftInput, {
880
+ disabled: props.disabled,
838
881
  value: entry?.apiKeyRef ?? "",
839
882
  width: "220px",
840
883
  onCommit: (value) => {
@@ -859,6 +902,7 @@ window.__ModuleLoader__.load({
859
902
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialPanel, {
860
903
  t,
861
904
  refs,
905
+ refOverride: row.resolution.apiKeyRef,
862
906
  description: props.description,
863
907
  credentials: props.credentials,
864
908
  onChanged: props.onCredentialChanged
@@ -878,6 +922,7 @@ window.__ModuleLoader__.load({
878
922
  const { t } = props;
879
923
  const snapshot = useSettingsValue(props.settings);
880
924
  const state = useStoreState(props.usageState);
925
+ const [writeError, setWriteError] = (0, react.useState)(void 0);
881
926
  (0, react.useEffect)(() => {
882
927
  props.usageState.refreshModels();
883
928
  props.usageState.refreshCredentials();
@@ -891,17 +936,27 @@ window.__ModuleLoader__.load({
891
936
  models: state.models,
892
937
  config,
893
938
  catalog: state.catalog,
894
- registry: state.modelRegistry
939
+ registry: state.modelRegistry,
940
+ endpointHints: state.endpointHints
895
941
  });
942
+ const mutate = async (ops) => {
943
+ if (!snapshot.writable) return;
944
+ setWriteError(void 0);
945
+ try {
946
+ await props.settings.mutate(ops);
947
+ } catch (error) {
948
+ setWriteError(error instanceof Error ? error.message : String(error));
949
+ }
950
+ };
896
951
  const write = (path, value) => {
897
- props.settings.mutate([{
952
+ mutate([{
898
953
  op: "set",
899
954
  path,
900
955
  value
901
956
  }]);
902
957
  };
903
958
  const clear = (path) => {
904
- props.settings.mutate([{
959
+ mutate([{
905
960
  op: "unset",
906
961
  path
907
962
  }]);
@@ -952,6 +1007,15 @@ window.__ModuleLoader__.load({
952
1007
  state.modelsError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
953
1008
  style: MUTED,
954
1009
  children: state.modelsError
1010
+ }),
1011
+ state.credentialsError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1012
+ style: MUTED,
1013
+ children: t("credentialFailed", { message: state.credentialsError })
1014
+ }),
1015
+ writeError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1016
+ role: "alert",
1017
+ style: MUTED,
1018
+ children: t("settingsFailed", { message: writeError })
955
1019
  })
956
1020
  ]
957
1021
  })
@@ -979,14 +1043,19 @@ window.__ModuleLoader__.load({
979
1043
  failure: row.resolution.key === void 0 ? void 0 : state.snapshots[row.resolution.key]?.error,
980
1044
  index,
981
1045
  total: rows.length,
1046
+ disabled: !snapshot.writable,
982
1047
  onMode: (mode) => write(["providers"], setProviderMode(config.providers, row.provider, mode)),
983
1048
  onMove: (delta) => {
984
- const next = reorderProviders(config.order, row.provider, delta);
1049
+ const next = reorderProviders(rows.map((candidate) => candidate.provider), row.provider, delta);
985
1050
  if (next !== void 0) write(["order"], next);
986
1051
  },
987
1052
  onField: (path, value) => write(path, value),
988
1053
  onClearField: (path) => clear(path),
989
- onCredentialChanged: () => void props.usageState.refreshCredentials()
1054
+ onCredentialChanged: () => {
1055
+ props.usageState.invalidate();
1056
+ props.usageState.refreshCredentials();
1057
+ props.usageState.refresh(true);
1058
+ }
990
1059
  }, row.provider))
991
1060
  ]
992
1061
  }),
@@ -1003,6 +1072,7 @@ window.__ModuleLoader__.load({
1003
1072
  }),
1004
1073
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DraftInput, {
1005
1074
  type: "number",
1075
+ disabled: !snapshot.writable,
1006
1076
  value: String(config.display.thresholdWarnPercent),
1007
1077
  width: "90px",
1008
1078
  onCommit: (value) => write(["display", "thresholdWarnPercent"], Number(value))
@@ -1013,6 +1083,7 @@ window.__ModuleLoader__.load({
1013
1083
  }),
1014
1084
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DraftInput, {
1015
1085
  type: "number",
1086
+ disabled: !snapshot.writable,
1016
1087
  value: String(config.display.thresholdCriticalPercent),
1017
1088
  width: "90px",
1018
1089
  onCommit: (value) => write(["display", "thresholdCriticalPercent"], Number(value))
@@ -1026,12 +1097,14 @@ window.__ModuleLoader__.load({
1026
1097
  children: t("intervalMinutes")
1027
1098
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DraftInput, {
1028
1099
  type: "number",
1100
+ disabled: !snapshot.writable,
1029
1101
  value: String(config.refresh.intervalMinutes),
1030
1102
  width: "90px",
1031
1103
  onCommit: (value) => write(["refresh", "intervalMinutes"], Number(value))
1032
1104
  })]
1033
1105
  }),
1034
1106
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1107
+ disabled: !snapshot.writable,
1035
1108
  checked: config.display.progressBar,
1036
1109
  label: t("progressBar"),
1037
1110
  onChange: (next) => write(["display", "progressBar"], next)
@@ -1123,7 +1196,9 @@ window.__ModuleLoader__.load({
1123
1196
  kind: "label",
1124
1197
  text: input.sourceLabel,
1125
1198
  stale: true,
1126
- ...snapshot.fetchedAt > 0 ? { staleSince: snapshot.fetchedAt } : {}
1199
+ staleSince: snapshot.fetchedAt,
1200
+ ...snapshot.error === void 0 ? {} : { errorKind: snapshot.error.kind },
1201
+ ...snapshot.error?.detail === void 0 ? {} : { errorDetail: snapshot.error.detail }
1127
1202
  } : {
1128
1203
  kind: "label",
1129
1204
  text: input.sourceLabel
@@ -1178,6 +1253,10 @@ window.__ModuleLoader__.load({
1178
1253
  const present = pieces.filter((piece) => piece !== void 0 && piece !== "");
1179
1254
  return present.length === 0 ? void 0 : present.join(" · ");
1180
1255
  }
1256
+ function failureReason(kind, detail, t) {
1257
+ if (kind === void 0) return detail;
1258
+ return detail === void 0 ? t(`error.${kind}`) : `${t(`error.${kind}`)}: ${detail}`;
1259
+ }
1181
1260
  function localTime(epochMs) {
1182
1261
  return new Date(epochMs).toLocaleTimeString();
1183
1262
  }
@@ -1195,20 +1274,17 @@ window.__ModuleLoader__.load({
1195
1274
  for (const segment of segments) switch (segment.kind) {
1196
1275
  case "label": {
1197
1276
  const staleHint = segment.stale === true ? t("staleHint") : void 0;
1198
- const refreshHint = segment.staleSince === void 0 ? void 0 : t("tipUpdated", { age: formatAge(segment.staleSince, now) });
1277
+ const tooltip = tooltipOf([
1278
+ staleHint,
1279
+ segment.staleSince === void 0 ? void 0 : t("tipUpdated", { age: formatAge(segment.staleSince, now) }),
1280
+ failureReason(segment.errorKind, segment.errorDetail, t),
1281
+ context
1282
+ ]);
1199
1283
  parts.push({
1200
1284
  kind: "label",
1201
1285
  text: segment.text,
1202
1286
  stale: segment.stale === true,
1203
- ...tooltipOf([
1204
- staleHint,
1205
- refreshHint,
1206
- context
1207
- ]) === void 0 ? {} : { tooltip: tooltipOf([
1208
- staleHint,
1209
- refreshHint,
1210
- context
1211
- ]) }
1287
+ ...tooltip === void 0 ? {} : { tooltip }
1212
1288
  });
1213
1289
  if (segment.staleSince !== void 0) parts.push({
1214
1290
  kind: "age",
@@ -1246,7 +1322,7 @@ window.__ModuleLoader__.load({
1246
1322
  break;
1247
1323
  }
1248
1324
  case "state": {
1249
- const reason = segment.errorKind === void 0 ? void 0 : segment.errorDetail === void 0 ? t(`error.${segment.errorKind}`) : `${t(`error.${segment.errorKind}`)}: ${segment.errorDetail}`;
1325
+ const reason = failureReason(segment.errorKind, segment.errorDetail, t);
1250
1326
  parts.push({
1251
1327
  kind: "state",
1252
1328
  state: segment.state,
@@ -1435,7 +1511,7 @@ window.__ModuleLoader__.load({
1435
1511
  * One read-only usage line: balance in API mode, 5h/7d quota in coding-plan mode.
1436
1512
  *
1437
1513
  * It reports the account's **current** reading, so it has exactly one home: the
1438
- * line under the composer. There is no per-turn copy — an account-level number
1514
+ * line above the composer. There is no per-turn copy — an account-level number
1439
1515
  * cannot honestly describe a single turn (see `slots.ts`).
1440
1516
  */
1441
1517
  function StatusLine(props) {
@@ -1449,9 +1525,19 @@ window.__ModuleLoader__.load({
1449
1525
  const status = statusOf(resolveProvider({
1450
1526
  provider: current.provider,
1451
1527
  config,
1452
- catalog: state.catalog
1528
+ catalog: state.catalog,
1529
+ endpointHint: state.endpointHints?.[current.provider]
1453
1530
  }), state.catalog.length === 0);
1454
- const snapshot = status.kind === "ready" ? state.snapshots[status.key] : void 0;
1531
+ if (status.kind === "hidden") return null;
1532
+ const kept = status.kind === "ready" ? state.snapshots[status.key] : void 0;
1533
+ const snapshot = kept === void 0 || state.error === void 0 ? kept : {
1534
+ ...kept,
1535
+ stale: true,
1536
+ error: {
1537
+ kind: "network",
1538
+ detail: state.error
1539
+ }
1540
+ };
1455
1541
  const sourceLabel = status.kind === "ready" ? state.catalog.find((entry) => entry.id === status.sourceId)?.displayName ?? status.sourceId : "";
1456
1542
  const fullParts = statusParts({
1457
1543
  segments: state.catalog.length === 0 && state.status === "error" && state.error !== void 0 ? [{
@@ -1491,7 +1577,7 @@ window.__ModuleLoader__.load({
1491
1577
  const zh = {
1492
1578
  nav: "用量状态",
1493
1579
  title: "用量状态",
1494
- intro: "为每个模型选择显示账户余额(API 模式)或套餐额度(Coding Plan 模式)。数据由宿主定时刷新,密钥存放在 DSH 凭据库中,本插件不保存明文。",
1580
+ intro: "为每个供应商选择显示账户余额(API 模式)或套餐额度(Coding Plan 模式)。数据由宿主定时刷新,密钥交给 DSH 凭据库保存,本插件不另存明文。",
1495
1581
  refreshNow: "立即刷新",
1496
1582
  refreshing: "刷新中…",
1497
1583
  lastChecked: "更新于 {time}",
@@ -1527,7 +1613,7 @@ window.__ModuleLoader__.load({
1527
1613
  credential: "密钥",
1528
1614
  credentialConfigured: "已配置(来源:{source})",
1529
1615
  credentialMissing: "未配置",
1530
- credentialLocked: "由环境变量提供,无法在此修改",
1616
+ credentialLocked: "当前凭据来源无法在此修改",
1531
1617
  credentialPlaceholder: "粘贴密钥…",
1532
1618
  credentialSave: "保存",
1533
1619
  credentialClear: "清除",
@@ -1537,7 +1623,8 @@ window.__ModuleLoader__.load({
1537
1623
  intervalMinutes: "空闲刷新间隔(分钟)",
1538
1624
  thresholdWarn: "黄色阈值(已用 %)",
1539
1625
  thresholdCritical: "红色阈值(已用 %)",
1540
- progressBar: "显示进度条",
1626
+ progressBar: "显示进度环",
1627
+ settingsFailed: "无法保存设置:{message}",
1541
1628
  refreshFailed: "刷新失败:{message}",
1542
1629
  "state.loading": "读取中…",
1543
1630
  "state.unconfigured": "未配置",
@@ -1561,12 +1648,14 @@ window.__ModuleLoader__.load({
1561
1648
  "window.5h": "5h",
1562
1649
  "window.1d": "1d",
1563
1650
  "window.7d": "7d",
1564
- "window.30d": "30d"
1651
+ "window.30d": "30d",
1652
+ "window.month": "月总额度",
1653
+ "window.month-code": "编程月用量"
1565
1654
  };
1566
1655
  const en = {
1567
1656
  nav: "Usage state",
1568
1657
  title: "Usage state",
1569
- intro: "Choose what to show for each model: account balance (API mode) or coding-plan quota (Coding Plan mode). The host refreshes on a timer, and keys live in the DSH credential store — this plugin never keeps a plaintext copy.",
1658
+ intro: "Choose what to show for each provider: account balance (API mode) or coding-plan quota (Coding Plan mode). The host owns refresh timing, and DSH stores credentials — this plugin keeps no separate plaintext copy.",
1570
1659
  refreshNow: "Refresh now",
1571
1660
  refreshing: "Refreshing…",
1572
1661
  lastChecked: "Updated {time}",
@@ -1602,7 +1691,7 @@ window.__ModuleLoader__.load({
1602
1691
  credential: "API key",
1603
1692
  credentialConfigured: "Configured ({source})",
1604
1693
  credentialMissing: "Not configured",
1605
- credentialLocked: "Provided by an environment variable; not editable here",
1694
+ credentialLocked: "This credential source cannot be edited here",
1606
1695
  credentialPlaceholder: "Paste the key…",
1607
1696
  credentialSave: "Save",
1608
1697
  credentialClear: "Clear",
@@ -1612,7 +1701,8 @@ window.__ModuleLoader__.load({
1612
1701
  intervalMinutes: "Idle refresh interval (minutes)",
1613
1702
  thresholdWarn: "Amber threshold (used %)",
1614
1703
  thresholdCritical: "Red threshold (used %)",
1615
- progressBar: "Show progress bar",
1704
+ progressBar: "Show progress ring",
1705
+ settingsFailed: "Could not save settings: {message}",
1616
1706
  refreshFailed: "Refresh failed: {message}",
1617
1707
  "state.loading": "Reading…",
1618
1708
  "state.unconfigured": "Not configured",
@@ -1636,7 +1726,9 @@ window.__ModuleLoader__.load({
1636
1726
  "window.5h": "5h",
1637
1727
  "window.1d": "1d",
1638
1728
  "window.7d": "7d",
1639
- "window.30d": "30d"
1729
+ "window.30d": "30d",
1730
+ "window.month": "Monthly total",
1731
+ "window.month-code": "Monthly code"
1640
1732
  };
1641
1733
  //#endregion
1642
1734
  //#region src/client/remote.ts
@@ -1660,6 +1752,9 @@ window.__ModuleLoader__.load({
1660
1752
  }
1661
1753
  //#endregion
1662
1754
  //#region src/client/settings-form.ts
1755
+ async function requireAccepted(write) {
1756
+ if (!await write) throw new Error("The host did not accept the settings write");
1757
+ }
1663
1758
  /**
1664
1759
  * Present one `configForms` entry as the settings scope the components consume.
1665
1760
  *
@@ -1696,13 +1791,13 @@ window.__ModuleLoader__.load({
1696
1791
  getSnapshot: snapshot,
1697
1792
  subscribe: (listener) => form.subscribe(listener),
1698
1793
  set: async (field, value) => {
1699
- await form.set(field, value);
1794
+ await requireAccepted(form.set(field, value));
1700
1795
  },
1701
1796
  unset: async (field) => {
1702
- await form.unset(field);
1797
+ await requireAccepted(form.unset(field));
1703
1798
  },
1704
1799
  mutate: async (ops, expectedRevision) => {
1705
- await form.mutate(ops, expectedRevision);
1800
+ await requireAccepted(form.mutate(ops, expectedRevision));
1706
1801
  }
1707
1802
  };
1708
1803
  }
@@ -1766,7 +1861,11 @@ window.__ModuleLoader__.load({
1766
1861
  credentialsError: void 0,
1767
1862
  modelsError: void 0
1768
1863
  };
1864
+ generation = 0;
1865
+ inflightGeneration = 0;
1866
+ inflightForce = false;
1769
1867
  inflight;
1868
+ forcedAfterInflight;
1770
1869
  constructor(deps) {
1771
1870
  this.deps = deps;
1772
1871
  }
@@ -1786,12 +1885,38 @@ window.__ModuleLoader__.load({
1786
1885
  };
1787
1886
  for (const listener of [...this.listeners]) listener();
1788
1887
  }
1888
+ /** A provider/credential identity changed: its previous RPC cannot restore old values. */
1889
+ invalidate() {
1890
+ this.generation++;
1891
+ this.forcedAfterInflight = void 0;
1892
+ this.publish({
1893
+ status: "loading",
1894
+ snapshots: {},
1895
+ credentials: {},
1896
+ checkedAt: void 0,
1897
+ error: void 0,
1898
+ credentialsError: void 0
1899
+ });
1900
+ }
1789
1901
  refresh(force = false) {
1790
- if (this.inflight !== void 0) return this.inflight;
1902
+ const generation = this.generation;
1903
+ if (this.inflight !== void 0 && this.inflightGeneration === generation) {
1904
+ if (!force || this.inflightForce) return this.inflight;
1905
+ if (this.forcedAfterInflight === void 0) {
1906
+ const queued = this.inflight.then(() => {
1907
+ if (this.generation === generation) return this.refresh(true);
1908
+ }).finally(() => {
1909
+ if (this.forcedAfterInflight === queued) this.forcedAfterInflight = void 0;
1910
+ });
1911
+ this.forcedAfterInflight = queued;
1912
+ }
1913
+ return this.forcedAfterInflight;
1914
+ }
1791
1915
  this.publish({ status: "loading" });
1792
1916
  const run = (async () => {
1793
1917
  try {
1794
1918
  const result = await this.deps.getState(force);
1919
+ if (generation !== this.generation) return;
1795
1920
  if (!result.ok) {
1796
1921
  this.publish({
1797
1922
  status: "error",
@@ -1802,19 +1927,22 @@ window.__ModuleLoader__.load({
1802
1927
  this.publish({
1803
1928
  status: "ready",
1804
1929
  catalog: result.value.sources,
1930
+ endpointHints: result.value.endpointHints ?? {},
1805
1931
  snapshots: result.value.snapshots,
1806
1932
  checkedAt: result.value.checkedAt,
1807
1933
  error: void 0
1808
1934
  });
1809
1935
  } catch (error) {
1810
- this.publish({
1936
+ if (generation === this.generation) this.publish({
1811
1937
  status: "error",
1812
1938
  error: messageOf(error)
1813
1939
  });
1814
1940
  }
1815
1941
  })().finally(() => {
1816
- this.inflight = void 0;
1942
+ if (this.inflight === run) this.inflight = void 0;
1817
1943
  });
1944
+ this.inflightGeneration = generation;
1945
+ this.inflightForce = force;
1818
1946
  this.inflight = run;
1819
1947
  return run;
1820
1948
  }
@@ -1843,8 +1971,10 @@ window.__ModuleLoader__.load({
1843
1971
  }
1844
1972
  }
1845
1973
  async refreshCredentials() {
1974
+ const generation = this.generation;
1846
1975
  try {
1847
1976
  const result = await this.deps.describeCredentials();
1977
+ if (generation !== this.generation) return;
1848
1978
  if (!result.ok) {
1849
1979
  this.publish({ credentialsError: result.error.message });
1850
1980
  return;
@@ -1854,7 +1984,7 @@ window.__ModuleLoader__.load({
1854
1984
  credentialsError: void 0
1855
1985
  });
1856
1986
  } catch (error) {
1857
- this.publish({ credentialsError: messageOf(error) });
1987
+ if (generation === this.generation) this.publish({ credentialsError: messageOf(error) });
1858
1988
  }
1859
1989
  }
1860
1990
  /**
@@ -1946,6 +2076,15 @@ window.__ModuleLoader__.load({
1946
2076
  store.refresh(false);
1947
2077
  if (store.getSnapshot().models.length === 0) store.refreshModels();
1948
2078
  }, POLL_INTERVAL_MS);
2079
+ const configurationIdentity = () => {
2080
+ const config = settings.getSnapshot().value;
2081
+ return config === void 0 ? void 0 : JSON.stringify([
2082
+ config.order,
2083
+ config.providers,
2084
+ config.sources
2085
+ ]);
2086
+ };
2087
+ let configuredIdentity = configurationIdentity();
1949
2088
  const disposers = [
1950
2089
  ctx.on("api-session/status", (...args) => {
1951
2090
  if (args[1] === false) store.refresh(false);
@@ -1954,7 +2093,13 @@ window.__ModuleLoader__.load({
1954
2093
  store.refresh(true);
1955
2094
  }),
1956
2095
  settings.subscribe(() => {
2096
+ const nextIdentity = configurationIdentity();
2097
+ if (nextIdentity !== configuredIdentity) {
2098
+ configuredIdentity = nextIdentity;
2099
+ store.invalidate();
2100
+ }
1957
2101
  store.refresh(false);
2102
+ store.refreshCredentials();
1958
2103
  })
1959
2104
  ];
1960
2105
  return () => {