@linxin666/dsh-usage 0.3.23 → 0.4.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 (38) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +7 -9
  3. package/README.zh.md +7 -9
  4. package/icon.svg +3 -0
  5. package/lib/client.js +621 -65
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +258 -271
  8. package/lib/types/client/UsageFootCard.d.ts +55 -0
  9. package/lib/types/client/UsageSectionCard.d.ts +3 -4
  10. package/lib/types/client/body-mutations.d.ts +45 -0
  11. package/lib/types/client/foot-card-mount.d.ts +34 -0
  12. package/lib/types/client/index.d.ts +21 -8
  13. package/lib/types/client/locales.d.ts +6 -4
  14. package/lib/types/core/provider-routes.d.ts +26 -0
  15. package/lib/types/core/types.d.ts +14 -1
  16. package/lib/types/host/access.d.ts +17 -0
  17. package/lib/types/host/http.d.ts +18 -1
  18. package/lib/types/host/pair-access.d.ts +34 -0
  19. package/lib/types/host/routes.d.ts +16 -7
  20. package/lib/types/host/usage-service.d.ts +32 -76
  21. package/lib/types/index.d.ts +14 -2
  22. package/package.json +20 -17
  23. package/src/client/UsageFootCard.tsx +300 -0
  24. package/src/client/UsageSectionCard.tsx +41 -23
  25. package/src/client/body-mutations.ts +133 -0
  26. package/src/client/foot-card-mount.tsx +112 -0
  27. package/src/client/index.ts +57 -16
  28. package/src/client/locales.ts +12 -8
  29. package/src/client/usage.module.css +188 -0
  30. package/src/core/adapters.ts +40 -6
  31. package/src/core/provider-routes.ts +37 -0
  32. package/src/core/types.ts +14 -1
  33. package/src/host/access.ts +21 -0
  34. package/src/host/http.ts +23 -1
  35. package/src/host/pair-access.ts +49 -0
  36. package/src/host/routes.ts +19 -10
  37. package/src/host/usage-service.ts +74 -218
  38. package/src/index.ts +59 -60
package/lib/client.js CHANGED
@@ -6,6 +6,7 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  let react_jsx_runtime = require("react/jsx-runtime");
9
+ let react_dom_client = require("react-dom/client");
9
10
  //#region \0dsh-store-engine
10
11
  const platform = ["@deepseek-ai/dsh-client", "-store"].join("");
11
12
  const legacy = ["@deepseek-ai/dsh-client-runtime", "/client"].join("");
@@ -97,10 +98,7 @@ window.__ModuleLoader__.load({
97
98
  "usage.config.title": "设置",
98
99
  "usage.config.enabled": "启用插件",
99
100
  "usage.config.pollIntervalSec": "轮询间隔(秒)",
100
- "usage.config.bubbleMode": "宠物气泡",
101
- "usage.config.bubbleMode.always": "常驻显示",
102
- "usage.config.bubbleMode.change": "仅变化时",
103
- "usage.config.bubbleMode.off": "关闭",
101
+ "usage.config.saveFailed": "保存失败:设置未被接受。",
104
102
  "usage.bank.title": "鲸元券",
105
103
  "usage.bank.hint": "官方 API 每消耗 100 万 tokens 铸造 1 鲸元;保存或分享这张票券。",
106
104
  "usage.bank.noUsage": "暂无 DeepSeek 官方用量数据(统计自插件启用起)",
@@ -110,7 +108,12 @@ window.__ModuleLoader__.load({
110
108
  "usage.bank.window": "统计窗口 {from} ~ {to}",
111
109
  "usage.bank.save": "保存图片",
112
110
  "usage.bank.share": "分享",
113
- "usage.bank.drawError": "票券生成失败:{error}"
111
+ "usage.bank.drawError": "票券生成失败:{error}",
112
+ "usage.foot.cost": "今日消费",
113
+ "usage.foot.noData": "今日暂无用量",
114
+ "usage.foot.open": "打开使用统计设置",
115
+ "usage.foot.collapse": "收起用量卡片",
116
+ "usage.foot.expand": "展开用量卡片"
114
117
  };
115
118
  /** English mirror; every zh key present. */
116
119
  const en = {
@@ -153,10 +156,7 @@ window.__ModuleLoader__.load({
153
156
  "usage.config.title": "Settings",
154
157
  "usage.config.enabled": "Enable plugin",
155
158
  "usage.config.pollIntervalSec": "Poll interval (seconds)",
156
- "usage.config.bubbleMode": "Pet bubble",
157
- "usage.config.bubbleMode.always": "Always visible",
158
- "usage.config.bubbleMode.change": "On change",
159
- "usage.config.bubbleMode.off": "Off",
159
+ "usage.config.saveFailed": "Save failed: the deployment did not accept the setting.",
160
160
  "usage.bank.title": "Whale-yuan voucher",
161
161
  "usage.bank.hint": "Every 1,000,000 tokens spent on the official API mint one whale yuan; save or share the note.",
162
162
  "usage.bank.noUsage": "No official DeepSeek usage yet (counting starts when the plugin is enabled)",
@@ -166,7 +166,12 @@ window.__ModuleLoader__.load({
166
166
  "usage.bank.window": "Window {from} - {to}",
167
167
  "usage.bank.save": "Save image",
168
168
  "usage.bank.share": "Share",
169
- "usage.bank.drawError": "Failed to render the voucher: {error}"
169
+ "usage.bank.drawError": "Failed to render the voucher: {error}",
170
+ "usage.foot.cost": "Today spend",
171
+ "usage.foot.noData": "No usage yet today",
172
+ "usage.foot.open": "Open usage statistics settings",
173
+ "usage.foot.collapse": "Collapse the usage card",
174
+ "usage.foot.expand": "Expand the usage card"
170
175
  };
171
176
  /**
172
177
  * Active dictionary, picked by the document language at call time. The
@@ -184,7 +189,7 @@ window.__ModuleLoader__.load({
184
189
  }
185
190
  //#endregion
186
191
  //#region \0dsh-css:packages/dsh-usage/src/client/usage.module.css.mjs
187
- const css = ".cvtkAW_section{color:inherit;flex-direction:column;gap:16px;display:flex}.cvtkAW_header{justify-content:space-between;align-items:center;gap:12px;display:flex}.cvtkAW_currentProvider{opacity:.75;font-size:13px}.cvtkAW_refreshBtn{appearance:none;color:inherit;font:inherit;cursor:pointer;opacity:.85;background:0 0;border:1px solid;border-radius:8px;padding:4px 12px;font-size:12px;transition:opacity .12s,background-color .12s}.cvtkAW_refreshBtn:hover:not(:disabled){opacity:1;background:color-mix(in srgb, currentColor 8%, transparent)}.cvtkAW_refreshBtn:disabled{cursor:default;opacity:.5}.cvtkAW_refreshBtn:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_tabs{border-bottom:1px solid color-mix(in srgb, currentColor 14%, transparent);gap:4px;display:flex}.cvtkAW_tab{appearance:none;color:inherit;font:inherit;cursor:pointer;opacity:.65;background:0 0;border:none;border-bottom:2px solid #0000;margin-bottom:-1px;padding:6px 14px;font-size:13px}.cvtkAW_tab:hover{opacity:.9}.cvtkAW_tab:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_tabActive{opacity:1;border-bottom-color:currentColor;font-weight:600}.cvtkAW_card{border:1px solid color-mix(in srgb, currentColor 14%, transparent);border-radius:12px;flex-direction:column;gap:10px;padding:14px 16px;display:flex}.cvtkAW_cardTitle{letter-spacing:.04em;text-transform:uppercase;opacity:.6;font-size:12px;font-weight:600}.cvtkAW_statRow{flex-wrap:wrap;gap:18px;display:flex}.cvtkAW_stat{flex-direction:column;gap:2px;display:flex}.cvtkAW_statValue{font-variant-numeric:tabular-nums;font-size:18px;font-weight:600}.cvtkAW_statLabel{opacity:.6;font-size:11px}.cvtkAW_providerRow{border-top:1px solid color-mix(in srgb, currentColor 8%, transparent);justify-content:space-between;align-items:center;gap:12px;padding:6px 0;font-size:13px;display:flex}.cvtkAW_providerRow:first-of-type{border-top:none}.cvtkAW_providerName{align-items:center;gap:8px;min-width:0;display:flex}.cvtkAW_providerTokens{font-variant-numeric:tabular-nums;opacity:.75;white-space:nowrap}.cvtkAW_providerBalance{font-variant-numeric:tabular-nums;white-space:nowrap;font-weight:600}.cvtkAW_currentBadge{border:1px solid color-mix(in srgb, currentColor 35%, transparent);opacity:.8;border-radius:999px;flex:none;padding:1px 7px;font-size:10px;font-weight:600}.cvtkAW_chart{flex-direction:column;gap:12px;display:flex}.cvtkAW_chartProvider{flex-direction:column;gap:4px;display:flex}.cvtkAW_chartHead{justify-content:space-between;align-items:baseline;gap:10px;font-size:13px;display:flex}.cvtkAW_chartTokens{font-variant-numeric:tabular-nums;opacity:.65;white-space:nowrap;font-size:11px}.cvtkAW_chartBar{background:color-mix(in srgb, currentColor 8%, transparent);border-radius:999px;height:8px;display:block;overflow:hidden}.cvtkAW_chartFill{background:color-mix(in srgb, currentColor 55%, transparent);border-radius:999px;height:100%;transition:width .3s;display:block}.cvtkAW_chartModel{opacity:.8;grid-template-columns:minmax(80px,180px) 1fr auto;align-items:center;gap:8px;padding-left:14px;font-size:11px;display:grid}.cvtkAW_chartModelName{text-overflow:ellipsis;white-space:nowrap;opacity:.8;overflow:hidden}.cvtkAW_chartModelBar{background:color-mix(in srgb, currentColor 6%, transparent);border-radius:999px;height:4px;display:block;overflow:hidden}.cvtkAW_chartModelFill{background:color-mix(in srgb, currentColor 35%, transparent);border-radius:999px;height:100%;transition:width .3s;display:block}.cvtkAW_trendAxis{opacity:.5;font-variant-numeric:tabular-nums;justify-content:space-between;font-size:10px;display:flex}.cvtkAW_muted{opacity:.6;font-size:12px}.cvtkAW_voucherPreview canvas{border-radius:8px;width:100%;height:auto;display:block}.cvtkAW_buttonRow{gap:8px;display:flex}.cvtkAW_errorLine{opacity:.75;font-size:12px}.cvtkAW_planCard{flex-direction:column;gap:8px;display:flex}.cvtkAW_planHead{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.cvtkAW_planName{font-size:14px;font-weight:600}.cvtkAW_windowRow{flex-direction:column;gap:4px;display:flex}.cvtkAW_windowLabel{opacity:.8;font-variant-numeric:tabular-nums;justify-content:space-between;font-size:12px;display:flex}.cvtkAW_bar{background:color-mix(in srgb, currentColor 10%, transparent);border-radius:999px;height:6px;overflow:hidden}.cvtkAW_barFill{background:color-mix(in srgb, currentColor 55%, transparent);border-radius:999px;height:100%;transition:width .3s}.cvtkAW_barWarn{background:#d97706}.cvtkAW_barLow{background:#dc2626}.cvtkAW_resetLine{opacity:.55;font-variant-numeric:tabular-nums;font-size:11px}.cvtkAW_settingsGrid{flex-wrap:wrap;align-items:center;gap:16px;display:flex}.cvtkAW_settingItem{align-items:center;gap:8px;font-size:13px;display:flex}.cvtkAW_settingItem input[type=checkbox]{accent-color:currentColor}.cvtkAW_settingItem input[type=number]{border:1px solid color-mix(in srgb, currentColor 25%, transparent);width:90px;color:inherit;font:inherit;background:0 0;border-radius:6px;padding:3px 8px;font-size:13px}.cvtkAW_settingItem select{border:1px solid color-mix(in srgb, currentColor 25%, transparent);color:inherit;font:inherit;background:0 0;border-radius:6px;padding:3px 8px;font-size:13px}.cvtkAW_settingItem input:focus-visible,.cvtkAW_settingItem select:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}";
192
+ const css = ".cvtkAW_section{color:inherit;flex-direction:column;gap:16px;display:flex}.cvtkAW_header{justify-content:space-between;align-items:center;gap:12px;display:flex}.cvtkAW_currentProvider{opacity:.75;font-size:13px}.cvtkAW_refreshBtn{appearance:none;color:inherit;font:inherit;cursor:pointer;opacity:.85;background:0 0;border:1px solid;border-radius:8px;padding:4px 12px;font-size:12px;transition:opacity .12s,background-color .12s}.cvtkAW_refreshBtn:hover:not(:disabled){opacity:1;background:color-mix(in srgb, currentColor 8%, transparent)}.cvtkAW_refreshBtn:disabled{cursor:default;opacity:.5}.cvtkAW_refreshBtn:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_tabs{border-bottom:1px solid color-mix(in srgb, currentColor 14%, transparent);gap:4px;display:flex}.cvtkAW_tab{appearance:none;color:inherit;font:inherit;cursor:pointer;opacity:.65;background:0 0;border:none;border-bottom:2px solid #0000;margin-bottom:-1px;padding:6px 14px;font-size:13px}.cvtkAW_tab:hover{opacity:.9}.cvtkAW_tab:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_tabActive{opacity:1;border-bottom-color:currentColor;font-weight:600}.cvtkAW_card{border:1px solid color-mix(in srgb, currentColor 14%, transparent);border-radius:12px;flex-direction:column;gap:10px;padding:14px 16px;display:flex}.cvtkAW_cardTitle{letter-spacing:.04em;text-transform:uppercase;opacity:.6;font-size:12px;font-weight:600}.cvtkAW_statRow{flex-wrap:wrap;gap:18px;display:flex}.cvtkAW_stat{flex-direction:column;gap:2px;display:flex}.cvtkAW_statValue{font-variant-numeric:tabular-nums;font-size:18px;font-weight:600}.cvtkAW_statLabel{opacity:.6;font-size:11px}.cvtkAW_providerRow{border-top:1px solid color-mix(in srgb, currentColor 8%, transparent);justify-content:space-between;align-items:center;gap:12px;padding:6px 0;font-size:13px;display:flex}.cvtkAW_providerRow:first-of-type{border-top:none}.cvtkAW_providerName{align-items:center;gap:8px;min-width:0;display:flex}.cvtkAW_providerTokens{font-variant-numeric:tabular-nums;opacity:.75;white-space:nowrap}.cvtkAW_providerBalance{font-variant-numeric:tabular-nums;white-space:nowrap;font-weight:600}.cvtkAW_currentBadge{border:1px solid color-mix(in srgb, currentColor 35%, transparent);opacity:.8;border-radius:999px;flex:none;padding:1px 7px;font-size:10px;font-weight:600}.cvtkAW_chart{flex-direction:column;gap:12px;display:flex}.cvtkAW_chartProvider{flex-direction:column;gap:4px;display:flex}.cvtkAW_chartHead{justify-content:space-between;align-items:baseline;gap:10px;font-size:13px;display:flex}.cvtkAW_chartTokens{font-variant-numeric:tabular-nums;opacity:.65;white-space:nowrap;font-size:11px}.cvtkAW_chartBar{background:color-mix(in srgb, currentColor 8%, transparent);border-radius:999px;height:8px;display:block;overflow:hidden}.cvtkAW_chartFill{background:color-mix(in srgb, currentColor 55%, transparent);border-radius:999px;height:100%;transition:width .3s;display:block}.cvtkAW_chartModel{opacity:.8;grid-template-columns:minmax(80px,180px) 1fr auto;align-items:center;gap:8px;padding-left:14px;font-size:11px;display:grid}.cvtkAW_chartModelName{text-overflow:ellipsis;white-space:nowrap;opacity:.8;overflow:hidden}.cvtkAW_chartModelBar{background:color-mix(in srgb, currentColor 6%, transparent);border-radius:999px;height:4px;display:block;overflow:hidden}.cvtkAW_chartModelFill{background:color-mix(in srgb, currentColor 35%, transparent);border-radius:999px;height:100%;transition:width .3s;display:block}.cvtkAW_trendAxis{opacity:.5;font-variant-numeric:tabular-nums;justify-content:space-between;font-size:10px;display:flex}.cvtkAW_muted{opacity:.6;font-size:12px}.cvtkAW_voucherPreview canvas{border-radius:8px;width:100%;height:auto;display:block}.cvtkAW_buttonRow{gap:8px;display:flex}.cvtkAW_errorLine{opacity:.75;font-size:12px}.cvtkAW_planCard{flex-direction:column;gap:8px;display:flex}.cvtkAW_planHead{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.cvtkAW_planName{font-size:14px;font-weight:600}.cvtkAW_windowRow{flex-direction:column;gap:4px;display:flex}.cvtkAW_windowLabel{opacity:.8;font-variant-numeric:tabular-nums;justify-content:space-between;font-size:12px;display:flex}.cvtkAW_bar{background:color-mix(in srgb, currentColor 10%, transparent);border-radius:999px;height:6px;overflow:hidden}.cvtkAW_barFill{background:color-mix(in srgb, currentColor 55%, transparent);border-radius:999px;height:100%;transition:width .3s}.cvtkAW_barWarn{background:#d97706}.cvtkAW_barLow{background:#dc2626}.cvtkAW_resetLine{opacity:.55;font-variant-numeric:tabular-nums;font-size:11px}.cvtkAW_settingsGrid{flex-wrap:wrap;align-items:center;gap:16px;display:flex}.cvtkAW_settingItem{align-items:center;gap:8px;font-size:13px;display:flex}.cvtkAW_settingItem input[type=checkbox]{accent-color:currentColor}.cvtkAW_settingItem input[type=number]{border:1px solid color-mix(in srgb, currentColor 25%, transparent);width:90px;color:inherit;font:inherit;background:0 0;border-radius:6px;padding:3px 8px;font-size:13px}.cvtkAW_settingItem select{border:1px solid color-mix(in srgb, currentColor 25%, transparent);color:inherit;font:inherit;background:0 0;border-radius:6px;padding:3px 8px;font-size:13px}.cvtkAW_settingItem input:focus-visible,.cvtkAW_settingItem select:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_footCard{box-sizing:border-box;background:color-mix(in srgb, currentColor 4%, transparent);width:100%;color:inherit;border:none;border-radius:12px;margin:2px 0 4px;transition:background-color .12s;position:relative}.cvtkAW_footCard:hover{background:var(--dsw-alias-interactive-bg-hover)}.cvtkAW_footCardCollapsed{background:0 0}.cvtkAW_footMain{appearance:none;box-sizing:border-box;width:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:12px;flex-direction:column;gap:4px;padding:8px;display:flex}.cvtkAW_footMain:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_footToggle{appearance:none;width:20px;height:20px;color:inherit;cursor:pointer;opacity:.55;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;transition:opacity .12s,background-color .12s;display:inline-flex;position:absolute;top:6px;right:6px}.cvtkAW_footCard:hover .cvtkAW_footToggle{opacity:.8}.cvtkAW_footToggle:hover{opacity:1;background:color-mix(in srgb, currentColor 10%, transparent)}.cvtkAW_footToggle:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_footToggle svg{display:block}.cvtkAW_footHead{justify-content:space-between;align-items:baseline;gap:8px;display:flex}.cvtkAW_footTitle{opacity:.65;white-space:nowrap;align-items:center;gap:6px;min-width:0;font-size:12px;display:inline-flex}.cvtkAW_footTitle svg{flex:none;display:block}.cvtkAW_footValue{font-variant-numeric:tabular-nums;white-space:nowrap;margin-right:18px;font-size:15px;font-weight:600}.cvtkAW_footLine{opacity:.7;font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.cvtkAW_footMeta{opacity:.5;font-variant-numeric:tabular-nums;font-size:10px}.cvtkAW_footCardCollapsed .cvtkAW_footMain{min-height:36px;padding:7px 8px}.cvtkAW_footStrip{white-space:nowrap;align-items:center;gap:6px;padding-right:20px;font-size:12px;display:flex;overflow:hidden}.cvtkAW_footStrip svg{opacity:.65;flex:none;display:block}.cvtkAW_footStripProvider{text-overflow:ellipsis;flex:none;max-width:45%;font-weight:500;overflow:hidden}.cvtkAW_footStripLabel{opacity:.65;text-overflow:ellipsis;overflow:hidden}.cvtkAW_footStripValue{font-variant-numeric:tabular-nums;margin-left:auto;font-weight:600}[data-dsh-frame][data-sidebar-collapsed] .cvtkAW_footCard,[data-sidebar-collapsed] .cvtkAW_footCard{display:none}";
188
193
  const tagId = "@linxin666/dsh-usage/packages/dsh-usage/src/client/usage.module.css";
189
194
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
190
195
  const tag = document.createElement("style");
@@ -214,6 +219,19 @@ window.__ModuleLoader__.load({
214
219
  "currentBadge": "cvtkAW_currentBadge",
215
220
  "currentProvider": "cvtkAW_currentProvider",
216
221
  "errorLine": "cvtkAW_errorLine",
222
+ "footCard": "cvtkAW_footCard",
223
+ "footCardCollapsed": "cvtkAW_footCardCollapsed",
224
+ "footHead": "cvtkAW_footHead",
225
+ "footLine": "cvtkAW_footLine",
226
+ "footMain": "cvtkAW_footMain",
227
+ "footMeta": "cvtkAW_footMeta",
228
+ "footStrip": "cvtkAW_footStrip",
229
+ "footStripLabel": "cvtkAW_footStripLabel",
230
+ "footStripProvider": "cvtkAW_footStripProvider",
231
+ "footStripValue": "cvtkAW_footStripValue",
232
+ "footTitle": "cvtkAW_footTitle",
233
+ "footToggle": "cvtkAW_footToggle",
234
+ "footValue": "cvtkAW_footValue",
217
235
  "header": "cvtkAW_header",
218
236
  "muted": "cvtkAW_muted",
219
237
  "planCard": "cvtkAW_planCard",
@@ -254,6 +272,10 @@ window.__ModuleLoader__.load({
254
272
  function str(value) {
255
273
  return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
256
274
  }
275
+ /** Clamp a provider percentage into the renderable 0-100 band. */
276
+ function clampPercent(value) {
277
+ return Math.max(0, Math.min(100, value));
278
+ }
257
279
  /** Format a number to a fixed 2-decimal display string. */
258
280
  function money(value) {
259
281
  return value.toFixed(2);
@@ -386,7 +408,28 @@ window.__ModuleLoader__.load({
386
408
  }
387
409
  }
388
410
  };
389
- /** GLM Coding Plan quota; auth is the RAW key without a Bearer prefix. */
411
+ /**
412
+ * GLM Coding Plan quota windows, keyed by the provider's own `unit` code
413
+ * rather than by row position so an added or reordered limit cannot shift a
414
+ * window's meaning: 3 = the rolling 5-hour window, 6 = the weekly window,
415
+ * 5 = the monthly one. Two row kinds carry quota — `TOKENS_LIMIT` (token
416
+ * plans) and `CREDIT_LIMIT` (credit plans, whose exact ratio is
417
+ * `currentValue` / `usage`) — while `TIME_LIMIT` is the MCP request cap and
418
+ * is not a plan window at all. Auth is the RAW key without a Bearer prefix.
419
+ */
420
+ const GLM_UNIT_KEYS = {
421
+ 3: "5h",
422
+ 5: "month",
423
+ 6: "week"
424
+ };
425
+ /** Percent from a credit row's exact ratio, else the provider's own percentage. */
426
+ function glmCreditPercent(row) {
427
+ const used = toNum(row.currentValue);
428
+ const limit = toNum(row.usage);
429
+ if (used !== void 0 && limit !== void 0 && limit > 0) return clampPercent(used / limit * 100);
430
+ const percentage = toNum(row.percentage);
431
+ return percentage === void 0 ? void 0 : clampPercent(percentage);
432
+ }
390
433
  function glmPlan(host, ids) {
391
434
  return {
392
435
  ids,
@@ -411,11 +454,17 @@ window.__ModuleLoader__.load({
411
454
  for (const entry of limits) {
412
455
  if (typeof entry !== "object" || entry === null) continue;
413
456
  const row = entry;
414
- const unit = toNum(row.unit);
415
- const percent = toNum(row.percentage);
457
+ const kind = str(row.type);
458
+ if (kind === "TIME_LIMIT") continue;
459
+ const key = GLM_UNIT_KEYS[toNum(row.unit) ?? NaN];
460
+ if (key === void 0) continue;
461
+ if (kind !== "TOKENS_LIMIT" && kind !== "CREDIT_LIMIT") continue;
416
462
  windows.push({
417
- key: unit === 3 ? "5h" : unit === 6 ? "week" : unit !== void 0 ? `unit-${unit}` : "window",
418
- percent: percent === void 0 ? void 0 : Math.max(0, Math.min(100, percent)),
463
+ key,
464
+ percent: kind === "CREDIT_LIMIT" ? glmCreditPercent(row) : (() => {
465
+ const percentage = toNum(row.percentage);
466
+ return percentage === void 0 ? void 0 : clampPercent(percentage);
467
+ })(),
419
468
  resetsAt: toIso(row.nextResetTime)
420
469
  });
421
470
  }
@@ -629,7 +678,7 @@ window.__ModuleLoader__.load({
629
678
  moonshotBalance("api.moonshot.ai", "USD", ["moonshotai"]),
630
679
  KIMI_CODING,
631
680
  glmPlan("open.bigmodel.cn", ["zai-coding-cn"]),
632
- glmPlan("api.z.ai", ["zai-coding"]),
681
+ glmPlan("api.z.ai", ["zai", "zai-coding"]),
633
682
  OPENCODE_GO,
634
683
  minimaxPlan("api.minimaxi.com", ["minimax-cn"]),
635
684
  minimaxPlan("api.minimax.io", ["minimax"]),
@@ -862,7 +911,7 @@ window.__ModuleLoader__.load({
862
911
  return "";
863
912
  }
864
913
  }
865
- function formatClock(ms) {
914
+ function formatClock$1(ms) {
866
915
  try {
867
916
  return new Date(ms).toLocaleTimeString([], {
868
917
  hour: "2-digit",
@@ -953,7 +1002,7 @@ window.__ModuleLoader__.load({
953
1002
  ]
954
1003
  });
955
1004
  }
956
- function balanceLine(provider) {
1005
+ function balanceLine$1(provider) {
957
1006
  if (provider.balance !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
958
1007
  className: usage_module_css_default.providerBalance,
959
1008
  children: [
@@ -989,7 +1038,7 @@ window.__ModuleLoader__.load({
989
1038
  })]
990
1039
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
991
1040
  className: usage_module_css_default.providerTokens,
992
- children: balanceLine(provider)
1041
+ children: balanceLine$1(provider)
993
1042
  })]
994
1043
  });
995
1044
  }
@@ -1125,7 +1174,7 @@ window.__ModuleLoader__.load({
1125
1174
  deepseekVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1126
1175
  className: usage_module_css_default.muted,
1127
1176
  "data-dsh-part": "peak-status",
1128
- children: t(deepseekPeriod.peak ? "usage.peak.on" : "usage.peak.off", { time: formatClock(deepseekPeriod.boundaryMs) })
1177
+ children: t(deepseekPeriod.peak ? "usage.peak.on" : "usage.peak.off", { time: formatClock$1(deepseekPeriod.boundaryMs) })
1129
1178
  }),
1130
1179
  snapshot.usage.today.totals.calls === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1131
1180
  className: usage_module_css_default.muted,
@@ -1507,35 +1556,52 @@ window.__ModuleLoader__.load({
1507
1556
  ]
1508
1557
  });
1509
1558
  }
1559
+ /**
1560
+ * The compact settings row. Both controls write through the shared form the
1561
+ * moment the user changes them, and the Host answers each write with a
1562
+ * boolean: a refused (or transport-failed) write is surfaced as a failed save,
1563
+ * because a value that did not land must never read as applied.
1564
+ */
1510
1565
  function SettingsRow(props) {
1511
1566
  const { settings, snapshot, value } = props;
1512
1567
  const disabled = snapshot === void 0 || !snapshot.writable;
1513
- const bubbleMode = typeof value.bubbleMode === "string" && [
1514
- "always",
1515
- "change",
1516
- "off"
1517
- ].includes(value.bubbleMode) ? value.bubbleMode : "always";
1568
+ const [failure, setFailure] = (0, react.useState)(void 0);
1569
+ const write = (field, next) => {
1570
+ setFailure(void 0);
1571
+ let answer;
1572
+ try {
1573
+ answer = settings.set(field, next);
1574
+ } catch (error) {
1575
+ setFailure(error instanceof Error ? error.message : String(error));
1576
+ return;
1577
+ }
1578
+ Promise.resolve(answer).then((accepted) => {
1579
+ if (!accepted) setFailure("");
1580
+ }, (error) => {
1581
+ setFailure(error instanceof Error ? error.message : String(error));
1582
+ });
1583
+ };
1518
1584
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1519
1585
  className: usage_module_css_default.card,
1520
1586
  "data-dsh-part": "settings-row",
1521
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1522
- className: usage_module_css_default.cardTitle,
1523
- children: t("usage.config.title")
1524
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1525
- className: usage_module_css_default.settingsGrid,
1526
- children: [
1527
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1587
+ children: [
1588
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1589
+ className: usage_module_css_default.cardTitle,
1590
+ children: t("usage.config.title")
1591
+ }),
1592
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1593
+ className: usage_module_css_default.settingsGrid,
1594
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1528
1595
  className: usage_module_css_default.settingItem,
1529
1596
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1530
1597
  type: "checkbox",
1531
1598
  checked: value.enabled ?? true,
1532
1599
  disabled,
1533
1600
  onChange: (event) => {
1534
- settings.set("enabled", event.target.checked);
1601
+ write("enabled", event.target.checked);
1535
1602
  }
1536
1603
  }), t("usage.config.enabled")]
1537
- }),
1538
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1604
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1539
1605
  className: usage_module_css_default.settingItem,
1540
1606
  children: [t("usage.config.pollIntervalSec"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1541
1607
  type: "number",
@@ -1545,38 +1611,513 @@ window.__ModuleLoader__.load({
1545
1611
  disabled,
1546
1612
  onChange: (event) => {
1547
1613
  const parsed = Number(event.target.value);
1548
- if (Number.isFinite(parsed) && parsed >= 30 && parsed <= 3600) settings.set("pollIntervalSec", Math.round(parsed));
1614
+ if (Number.isFinite(parsed) && parsed >= 30 && parsed <= 3600) write("pollIntervalSec", Math.round(parsed));
1549
1615
  }
1550
1616
  })]
1551
- }),
1552
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1553
- className: usage_module_css_default.settingItem,
1554
- children: [t("usage.config.bubbleMode"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1555
- value: bubbleMode,
1556
- disabled,
1557
- onChange: (event) => {
1558
- settings.set("bubbleMode", event.target.value);
1559
- },
1560
- children: [
1561
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1562
- value: "always",
1563
- children: t("usage.config.bubbleMode.always")
1564
- }),
1565
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1566
- value: "change",
1567
- children: t("usage.config.bubbleMode.change")
1568
- }),
1569
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1570
- value: "off",
1571
- children: t("usage.config.bubbleMode.off")
1572
- })
1573
- ]
1617
+ })]
1618
+ }),
1619
+ failure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1620
+ className: usage_module_css_default.errorLine,
1621
+ role: "status",
1622
+ children: [t("usage.config.saveFailed"), failure === "" ? "" : " - " + failure]
1623
+ })
1624
+ ]
1625
+ });
1626
+ }
1627
+ //#endregion
1628
+ //#region src/client/body-mutations.ts
1629
+ /** Cross-bundle registry key; `Symbol.for` so every module copy agrees. */
1630
+ const HUB_KEY = Symbol.for("dsh-web.body-mutation-hub");
1631
+ const INVALIDATION_ONLY = Symbol.for("dsh-web.body-mutation-invalidation");
1632
+ function needsRecords(subscribers) {
1633
+ for (const listener of subscribers) if (!listener[INVALIDATION_ONLY]) return true;
1634
+ return false;
1635
+ }
1636
+ /**
1637
+ * Subscribe to a coalesced DOM re-check without retaining mutation records.
1638
+ * The marked wrapper also works with an older hub, which delivers records
1639
+ * that it simply ignores until a page reload picks up the updated hub.
1640
+ */
1641
+ function subscribeBodyInvalidations(subscriber) {
1642
+ const listener = () => {
1643
+ subscriber();
1644
+ };
1645
+ listener[INVALIDATION_ONLY] = true;
1646
+ return subscribeBodyMutations(listener);
1647
+ }
1648
+ /**
1649
+ * Subscribe to body-level childList mutations.
1650
+ * @param subscriber - called at most once per animation frame with the records
1651
+ * collected since the previous flush; must be safe to run repeatedly.
1652
+ * @returns the disposer removing this subscriber (and the observer when it was
1653
+ * the last one).
1654
+ */
1655
+ function subscribeBodyMutations(subscriber) {
1656
+ if (typeof globalThis === "undefined" || typeof document === "undefined") return () => {};
1657
+ if (typeof MutationObserver !== "function") return () => {};
1658
+ const registry = globalThis;
1659
+ let hub = registry[HUB_KEY];
1660
+ if (hub === void 0) {
1661
+ const subscribers = /* @__PURE__ */ new Set();
1662
+ const created = {
1663
+ observer: void 0,
1664
+ subscribers,
1665
+ pending: [],
1666
+ scheduled: false
1667
+ };
1668
+ const flush = () => {
1669
+ created.frame = void 0;
1670
+ created.scheduled = false;
1671
+ const batch = created.pending;
1672
+ created.pending = [];
1673
+ for (const listener of [...subscribers]) {
1674
+ if (!subscribers.has(listener)) continue;
1675
+ try {
1676
+ listener(batch);
1677
+ } catch {}
1678
+ }
1679
+ };
1680
+ const schedule = () => {
1681
+ if (created.scheduled) return;
1682
+ created.scheduled = true;
1683
+ if (typeof requestAnimationFrame === "function") created.frame = requestAnimationFrame(flush);
1684
+ else flush();
1685
+ };
1686
+ created.observer = new MutationObserver((records) => {
1687
+ if (needsRecords(subscribers)) for (const record of records) created.pending.push(record);
1688
+ schedule();
1689
+ });
1690
+ created.observer.observe(document.body ?? document.documentElement, {
1691
+ childList: true,
1692
+ subtree: true
1693
+ });
1694
+ registry[HUB_KEY] = created;
1695
+ hub = created;
1696
+ }
1697
+ const active = hub;
1698
+ active.subscribers.add(subscriber);
1699
+ let subscribed = true;
1700
+ return () => {
1701
+ if (!subscribed) return;
1702
+ subscribed = false;
1703
+ active.subscribers.delete(subscriber);
1704
+ if (!needsRecords(active.subscribers)) active.pending = [];
1705
+ if (active.subscribers.size === 0 && registry[HUB_KEY] === active) {
1706
+ active.observer.disconnect();
1707
+ if (active.frame !== void 0 && typeof cancelAnimationFrame === "function") cancelAnimationFrame(active.frame);
1708
+ active.frame = void 0;
1709
+ active.pending = [];
1710
+ active.scheduled = false;
1711
+ delete registry[HUB_KEY];
1712
+ }
1713
+ };
1714
+ }
1715
+ //#endregion
1716
+ //#region src/client/UsageFootCard.tsx
1717
+ /**
1718
+ * Sidebar foot card: the compact usage glance seated below the shell sidebar's
1719
+ * Settings row. It is deliberately a GLANCE, not the panel the 2026-09-18
1720
+ * removal took out. Two states, persisted across reloads:
1721
+ *
1722
+ * - expanded (default): a price-first headline (today's estimated spend,
1723
+ * falling back to today's tokens when nothing priced was recorded), a
1724
+ * tokens/calls line, up to two configured-provider balances, and an
1725
+ * updated-at footer;
1726
+ * - collapsed: a one-line strip with the gauge glyph, the spending
1727
+ * provider's name, the label and the headline value.
1728
+ *
1729
+ * The card body is one button that opens the settings panel on the usage
1730
+ * section, so detail lives in exactly one place; the corner chevron toggles
1731
+ * the collapse state (sibling buttons — buttons inside a button are invalid
1732
+ * HTML).
1733
+ *
1734
+ * Data comes from the same shared store the settings section reads; the card
1735
+ * runs its own relaxed poll loop (30 s, visible-tab only) because the sidebar
1736
+ * foot is permanently mounted. The card disappears while the plugin is
1737
+ * disabled (settings flag) or while the host serves no usage routes (a 404
1738
+ * means the host half is off), matching the section's own gating.
1739
+ * @module @linxin666/dsh-usage/client/UsageFootCard
1740
+ */
1741
+ /** Poll cadence of the permanently seated card; the section's 10 s loop stays its own. */
1742
+ const FOOT_CARD_POLL_MS = 3e4;
1743
+ /** localStorage key holding the collapsed flag ('1' = collapsed strip). */
1744
+ const FOOT_CARD_COLLAPSED_KEY = "dsh-usage.foot-card.collapsed";
1745
+ /** At most this many provider balances render before a +N overflow marker. */
1746
+ const BALANCE_CAP = 2;
1747
+ /** Read the persisted collapsed flag (absent = expanded). */
1748
+ function readFootCardCollapsed() {
1749
+ try {
1750
+ return window.localStorage.getItem(FOOT_CARD_COLLAPSED_KEY) === "1";
1751
+ } catch {
1752
+ return false;
1753
+ }
1754
+ }
1755
+ /** Persist the collapsed flag; storage failures keep the session state. */
1756
+ function writeFootCardCollapsed(collapsed) {
1757
+ try {
1758
+ window.localStorage.setItem(FOOT_CARD_COLLAPSED_KEY, collapsed ? "1" : "0");
1759
+ } catch {}
1760
+ }
1761
+ /** Inline gauge glyph, 14px beside the card title. */
1762
+ function GaugeIcon() {
1763
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1764
+ viewBox: "0 0 16 16",
1765
+ width: "14",
1766
+ height: "14",
1767
+ fill: "none",
1768
+ stroke: "currentColor",
1769
+ strokeWidth: "1.3",
1770
+ strokeLinecap: "round",
1771
+ strokeLinejoin: "round",
1772
+ "aria-hidden": "true",
1773
+ children: [
1774
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.5 12.5a7 7 0 0 1 11 -4.3" }),
1775
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 12.5V8.2l2.9-2.1" }),
1776
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1777
+ cx: "8",
1778
+ cy: "12.5",
1779
+ r: "1"
1780
+ })
1781
+ ]
1782
+ });
1783
+ }
1784
+ /** Disclosure chevrons: down collapses the expanded card, up expands the strip. */
1785
+ function ChevronIcon(props) {
1786
+ return props.collapsed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1787
+ viewBox: "0 0 16 16",
1788
+ width: "12",
1789
+ height: "12",
1790
+ fill: "none",
1791
+ stroke: "currentColor",
1792
+ strokeWidth: "1.4",
1793
+ strokeLinecap: "round",
1794
+ strokeLinejoin: "round",
1795
+ "aria-hidden": "true",
1796
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m4 10 4-4 4 4" })
1797
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1798
+ viewBox: "0 0 16 16",
1799
+ width: "12",
1800
+ height: "12",
1801
+ fill: "none",
1802
+ stroke: "currentColor",
1803
+ strokeWidth: "1.4",
1804
+ strokeLinecap: "round",
1805
+ strokeLinejoin: "round",
1806
+ "aria-hidden": "true",
1807
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m4 6 4 4 4-4" })
1808
+ });
1809
+ }
1810
+ /** A provider row worth showing a balance for: configured, with a probed balance. */
1811
+ function balanceRow(provider) {
1812
+ const supported = provider.balanceSupported === true || provider.balanceSupported === void 0 && provider.supported;
1813
+ return provider.credential !== "none" && supported && provider.balance !== void 0;
1814
+ }
1815
+ /** Currency-symbol prefix for the priced balances (CNY/USD), else the ISO code. */
1816
+ function formatBalance(balance) {
1817
+ const currency = balance.currency.toUpperCase();
1818
+ if (currency === "CNY") return "¥" + balance.totalBalance;
1819
+ if (currency === "USD") return "$" + balance.totalBalance;
1820
+ return currency + " " + balance.totalBalance;
1821
+ }
1822
+ function formatClock(ms) {
1823
+ try {
1824
+ return new Date(ms).toLocaleTimeString([], {
1825
+ hour: "2-digit",
1826
+ minute: "2-digit"
1827
+ });
1828
+ } catch {
1829
+ return "";
1830
+ }
1831
+ }
1832
+ /**
1833
+ * Display name of the provider owning today's priced spend: the day's highest
1834
+ * cost row, else the current session route, resolved through the provider
1835
+ * snapshot (an older host without the day rows falls back to the current
1836
+ * route). Undefined when there is no route to name.
1837
+ */
1838
+ function spendProvider(snapshot) {
1839
+ let top;
1840
+ for (const row of snapshot.usage.today.providers) {
1841
+ if (row.totals.cost <= 0) continue;
1842
+ if (top === void 0 || row.totals.cost > top.totals.cost) top = row;
1843
+ }
1844
+ const route = top?.provider ?? snapshot.current.provider;
1845
+ if (route === void 0) return void 0;
1846
+ return snapshot.providers.find((entry) => entry.provider === route)?.displayName ?? (route === snapshot.current.provider ? snapshot.current.displayName : void 0) ?? route;
1847
+ }
1848
+ /**
1849
+ * The headline triple: today's priced spend, else today's tokens, else zero.
1850
+ * tokenFallback tells the sub-line to keep only the call count instead of
1851
+ * repeating the total the headline already shows; provider names the spender
1852
+ * beside the collapsed cost headline.
1853
+ */
1854
+ function headline(snapshot) {
1855
+ const totals = snapshot.usage.today.totals;
1856
+ if (totals.cost > 0) return {
1857
+ label: t("usage.foot.cost"),
1858
+ value: "¥" + totals.cost.toFixed(2),
1859
+ tokenFallback: false,
1860
+ provider: spendProvider(snapshot)
1861
+ };
1862
+ if (totals.calls > 0) return {
1863
+ label: t("usage.today"),
1864
+ value: formatTokens(totalTokens(totals)) + " tokens",
1865
+ tokenFallback: true
1866
+ };
1867
+ return {
1868
+ label: t("usage.foot.cost"),
1869
+ value: "¥0.00",
1870
+ tokenFallback: false
1871
+ };
1872
+ }
1873
+ /** The tokens/calls sub-line under the headline; a quiet no-data line on an empty day. */
1874
+ function usageLine(snapshot, tokenFallback) {
1875
+ const totals = snapshot.usage.today.totals;
1876
+ if (totals.calls === 0) return t("usage.foot.noData");
1877
+ const calls = t("usage.calls", { n: totals.calls });
1878
+ return tokenFallback ? calls : formatTokens(totalTokens(totals)) + " tokens · " + calls;
1879
+ }
1880
+ /** The balance line: up to BALANCE_CAP providers plus a +N overflow marker. */
1881
+ function balanceLine(snapshot) {
1882
+ const rows = snapshot.providers.filter(balanceRow);
1883
+ if (rows.length === 0) return void 0;
1884
+ return rows.slice(0, BALANCE_CAP).map((provider) => provider.displayName + " " + formatBalance(provider.balance)).join(" · ") + (rows.length > BALANCE_CAP ? " +" + String(rows.length - BALANCE_CAP) : "");
1885
+ }
1886
+ /** Card chrome classes for the current collapse state. */
1887
+ function cardClass(collapsed) {
1888
+ return collapsed ? usage_module_css_default.footCard + " " + usage_module_css_default.footCardCollapsed : usage_module_css_default.footCard;
1889
+ }
1890
+ /**
1891
+ * Render the foot card content for the current store snapshot.
1892
+ * @param props - store, poller, settings form and open callback from the mount.
1893
+ * @returns the card, or null while disabled / host-off.
1894
+ */
1895
+ function UsageFootCard(props) {
1896
+ const { store, poll, onOpen, settings, locale } = props;
1897
+ const ui = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot);
1898
+ const [, bump] = (0, react.useState)(0);
1899
+ (0, react.useEffect)(() => settings.subscribe(() => bump((count) => count + 1)), [settings]);
1900
+ (0, react.useEffect)(() => locale?.subscribe(() => bump((count) => count + 1)), [locale]);
1901
+ const enabled = settings.getSnapshot().value?.enabled ?? true;
1902
+ const [collapsed, setCollapsed] = (0, react.useState)(readFootCardCollapsed);
1903
+ const toggleCollapsed = () => {
1904
+ setCollapsed((current) => {
1905
+ writeFootCardCollapsed(!current);
1906
+ return !current;
1907
+ });
1908
+ };
1909
+ (0, react.useEffect)(() => {
1910
+ if (!enabled) return void 0;
1911
+ poll();
1912
+ let timer;
1913
+ const start = () => {
1914
+ if (timer === void 0 && document.visibilityState === "visible") timer = window.setInterval(poll, FOOT_CARD_POLL_MS);
1915
+ };
1916
+ const onVisibility = () => {
1917
+ if (document.visibilityState === "visible") {
1918
+ poll();
1919
+ start();
1920
+ } else if (timer !== void 0) {
1921
+ window.clearInterval(timer);
1922
+ timer = void 0;
1923
+ }
1924
+ };
1925
+ start();
1926
+ document.addEventListener("visibilitychange", onVisibility);
1927
+ return () => {
1928
+ if (timer !== void 0) window.clearInterval(timer);
1929
+ document.removeEventListener("visibilitychange", onVisibility);
1930
+ };
1931
+ }, [poll, enabled]);
1932
+ if (!enabled) return null;
1933
+ const snapshot = ui.snapshot;
1934
+ if (snapshot === null && ui.status === "error" && /failed: 404/.test(ui.error ?? "")) return null;
1935
+ const toggle = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1936
+ type: "button",
1937
+ className: usage_module_css_default.footToggle,
1938
+ "data-dsh-part": "foot-card-toggle",
1939
+ "aria-label": t(collapsed ? "usage.foot.expand" : "usage.foot.collapse"),
1940
+ title: t(collapsed ? "usage.foot.expand" : "usage.foot.collapse"),
1941
+ onClick: toggleCollapsed,
1942
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronIcon, { collapsed })
1943
+ });
1944
+ if (snapshot === null) {
1945
+ const quiet = ui.status === "error" ? t("usage.error", { error: ui.error ?? "" }) : t("usage.loading");
1946
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1947
+ className: cardClass(collapsed),
1948
+ "data-dsh-plugin": "usage",
1949
+ "data-dsh-part": "foot-card",
1950
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1951
+ type: "button",
1952
+ className: usage_module_css_default.footMain,
1953
+ "data-dsh-part": "foot-card-main",
1954
+ "aria-label": t("usage.foot.open"),
1955
+ title: t("usage.foot.open"),
1956
+ onClick: onOpen,
1957
+ children: collapsed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1958
+ className: usage_module_css_default.footStrip,
1959
+ "data-dsh-part": "foot-card-strip",
1960
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GaugeIcon, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1961
+ className: usage_module_css_default.footStripValue,
1962
+ children: "—"
1963
+ })]
1964
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1965
+ className: usage_module_css_default.footHead,
1966
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1967
+ className: usage_module_css_default.footTitle,
1968
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GaugeIcon, {}), t("usage.title")]
1969
+ })
1970
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1971
+ className: usage_module_css_default.footLine,
1972
+ children: quiet
1973
+ })] })
1974
+ }), toggle]
1975
+ });
1976
+ }
1977
+ const head = headline(snapshot);
1978
+ const balances = balanceLine(snapshot);
1979
+ if (collapsed) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1980
+ className: cardClass(true),
1981
+ "data-dsh-plugin": "usage",
1982
+ "data-dsh-part": "foot-card",
1983
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1984
+ type: "button",
1985
+ className: usage_module_css_default.footMain,
1986
+ "data-dsh-part": "foot-card-main",
1987
+ "aria-label": t("usage.foot.open"),
1988
+ title: t("usage.foot.open"),
1989
+ onClick: onOpen,
1990
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1991
+ className: usage_module_css_default.footStrip,
1992
+ "data-dsh-part": "foot-card-strip",
1993
+ children: [
1994
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GaugeIcon, {}),
1995
+ head.provider !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1996
+ className: usage_module_css_default.footStripProvider,
1997
+ children: head.provider
1998
+ }),
1999
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2000
+ className: usage_module_css_default.footStripLabel,
2001
+ children: head.label
2002
+ }),
2003
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2004
+ className: usage_module_css_default.footStripValue,
2005
+ children: head.value
2006
+ })
2007
+ ]
2008
+ })
2009
+ }), toggle]
2010
+ });
2011
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2012
+ className: cardClass(false),
2013
+ "data-dsh-plugin": "usage",
2014
+ "data-dsh-part": "foot-card",
2015
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2016
+ type: "button",
2017
+ className: usage_module_css_default.footMain,
2018
+ "data-dsh-part": "foot-card-main",
2019
+ "aria-label": t("usage.foot.open"),
2020
+ title: t("usage.foot.open"),
2021
+ onClick: onOpen,
2022
+ children: [
2023
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2024
+ className: usage_module_css_default.footHead,
2025
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2026
+ className: usage_module_css_default.footTitle,
2027
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GaugeIcon, {}), head.label]
2028
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2029
+ className: usage_module_css_default.footValue,
2030
+ children: head.value
1574
2031
  })]
2032
+ }),
2033
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2034
+ className: usage_module_css_default.footLine,
2035
+ "data-dsh-part": "foot-card-usage",
2036
+ children: usageLine(snapshot, head.tokenFallback)
2037
+ }),
2038
+ balances !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2039
+ className: usage_module_css_default.footLine,
2040
+ "data-dsh-part": "foot-card-balances",
2041
+ children: balances
2042
+ }),
2043
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2044
+ className: usage_module_css_default.footMeta,
2045
+ children: t("usage.updated", { time: formatClock(snapshot.updatedAt) })
1575
2046
  })
1576
2047
  ]
1577
- })]
2048
+ }), toggle]
1578
2049
  });
1579
2050
  }
2051
+ /** The shell sidebar's foot area (footer actions + the Settings row), when mounted. */
2052
+ function footArea() {
2053
+ return document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]")?.querySelector("[class*=\"footArea\"]") ?? void 0;
2054
+ }
2055
+ /**
2056
+ * Open the settings panel on the usage section. The shell owns the panel's
2057
+ * open state and exposes no service for it, so the card replays the user's
2058
+ * own path: activate the sidebar Settings trigger, then pick the nav row
2059
+ * carrying the section's (localized) label. Every step degrades silently —
2060
+ * with the panel already open only the nav pick runs, and a missing row
2061
+ * leaves the panel on its default section.
2062
+ * @param label - the usage section's nav label in the active locale.
2063
+ */
2064
+ function openUsageSettings(label) {
2065
+ /** Click the nav row carrying the usage label inside the open panel. */
2066
+ const pick = (panel) => {
2067
+ const wanted = label();
2068
+ for (const row of panel.querySelectorAll("nav button")) if (row.textContent !== null && row.textContent.includes(wanted)) {
2069
+ row.click();
2070
+ return;
2071
+ }
2072
+ };
2073
+ const open = document.querySelector("[role=\"dialog\"]");
2074
+ if (open !== null) {
2075
+ pick(open);
2076
+ return;
2077
+ }
2078
+ (document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]")?.querySelector("[class*=\"settingsArea\"] button"))?.click();
2079
+ let tries = 0;
2080
+ const attempt = () => {
2081
+ const panel = document.querySelector("[role=\"dialog\"]");
2082
+ if (panel !== null) {
2083
+ pick(panel);
2084
+ return;
2085
+ }
2086
+ tries += 1;
2087
+ if (tries <= 20) window.setTimeout(attempt, 50);
2088
+ };
2089
+ window.setTimeout(attempt, 0);
2090
+ }
2091
+ /**
2092
+ * Mount the sidebar foot card directly above the Settings row.
2093
+ * @param props - the store/poll/settings/open inputs of the apply body.
2094
+ * @returns disposer removing the container and its observers.
2095
+ */
2096
+ function mountUsageFootCard(props) {
2097
+ if (typeof document !== "undefined" && document.querySelector("[data-dsh-usage-foot-card]") !== null) return () => {};
2098
+ const container = document.createElement("div");
2099
+ container.setAttribute("data-dsh-usage-foot-card", "");
2100
+ const root = (0, react_dom_client.createRoot)(container);
2101
+ root.render((0, react.createElement)(UsageFootCard, props));
2102
+ /** Keep the container directly above the Settings seat inside the foot area. */
2103
+ const place = () => {
2104
+ const foot = footArea();
2105
+ if (foot === void 0) return;
2106
+ const settings = foot.querySelector("[class*=\"settingsArea\"]");
2107
+ if (settings !== null) {
2108
+ if (container.nextElementSibling !== settings) foot.insertBefore(container, settings);
2109
+ return;
2110
+ }
2111
+ if (foot.lastElementChild !== container) foot.append(container);
2112
+ };
2113
+ place();
2114
+ const unsubscribeBody = subscribeBodyInvalidations(place);
2115
+ return () => {
2116
+ unsubscribeBody();
2117
+ root.unmount();
2118
+ container.remove();
2119
+ };
2120
+ }
1580
2121
  //#endregion
1581
2122
  //#region src/client/index.ts
1582
2123
  /** Hard ceiling for one usage API call; a stalled host must not pile up requests. */
@@ -1593,7 +2134,7 @@ window.__ModuleLoader__.load({
1593
2134
  overview: () => usageFetch("/api/dsh-usage/overview", "GET"),
1594
2135
  refresh: () => usageFetch("/api/dsh-usage/refresh", "POST")
1595
2136
  };
1596
- /** Settings namespace the section edits (the host plugin registers it). */
2137
+ /** Settings namespace the section edits (dsh-web-settings maps it onto this row's profile entry id). */
1597
2138
  const USAGE_SETTINGS_NS = "dsh-usage";
1598
2139
  /** First-level nav position: directly below the Workshop section (order 150). */
1599
2140
  const SECTION_ORDER = 151;
@@ -1602,7 +2143,7 @@ window.__ModuleLoader__.load({
1602
2143
  "slots",
1603
2144
  "locale",
1604
2145
  "connection",
1605
- "settingsScope",
2146
+ "configForms",
1606
2147
  "remote"
1607
2148
  ];
1608
2149
  /**
@@ -1622,7 +2163,8 @@ window.__ModuleLoader__.load({
1622
2163
  return () => {};
1623
2164
  }
1624
2165
  }, "dsh-usage: dictionaries");
1625
- const settingsScope = (ctx.get("webUiSettings") ?? ctx.settingsScope).bind({ namespace: USAGE_SETTINGS_NS });
2166
+ const binder = ctx.get("webUiSettings");
2167
+ const settingsForm = binder !== void 0 ? binder.bind({ namespace: USAGE_SETTINGS_NS }) : ctx.configForms.get(USAGE_SETTINGS_NS);
1626
2168
  const store = createUsageStore().create();
1627
2169
  let pollSeq = 0;
1628
2170
  const poll = () => {
@@ -1651,8 +2193,22 @@ window.__ModuleLoader__.load({
1651
2193
  store,
1652
2194
  poll,
1653
2195
  refresh,
1654
- settings: settingsScope
2196
+ settings: settingsForm
1655
2197
  });
2198
+ const disposeFootCard = mountUsageFootCard({
2199
+ store,
2200
+ poll,
2201
+ settings: settingsForm,
2202
+ onOpen: () => {
2203
+ openUsageSettings(() => t("usage.title"));
2204
+ },
2205
+ locale: ctx.locale
2206
+ });
2207
+ ctx.effect(() => () => {
2208
+ try {
2209
+ disposeFootCard();
2210
+ } catch {}
2211
+ }, "dsh-usage: sidebar foot card");
1656
2212
  ctx.slots.inject("settings.section", () => {
1657
2213
  try {
1658
2214
  const unregister = ctx.slots.register({