@linxin666/dsh-usage 0.3.24 → 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.
package/lib/client.js CHANGED
@@ -5,8 +5,8 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
- let react_dom_client = require("react-dom/client");
9
8
  let react_jsx_runtime = require("react/jsx-runtime");
9
+ let react_dom_client = require("react-dom/client");
10
10
  //#region \0dsh-store-engine
11
11
  const platform = ["@deepseek-ai/dsh-client", "-store"].join("");
12
12
  const legacy = ["@deepseek-ai/dsh-client-runtime", "/client"].join("");
@@ -98,6 +98,7 @@ window.__ModuleLoader__.load({
98
98
  "usage.config.title": "设置",
99
99
  "usage.config.enabled": "启用插件",
100
100
  "usage.config.pollIntervalSec": "轮询间隔(秒)",
101
+ "usage.config.saveFailed": "保存失败:设置未被接受。",
101
102
  "usage.bank.title": "鲸元券",
102
103
  "usage.bank.hint": "官方 API 每消耗 100 万 tokens 铸造 1 鲸元;保存或分享这张票券。",
103
104
  "usage.bank.noUsage": "暂无 DeepSeek 官方用量数据(统计自插件启用起)",
@@ -108,15 +109,11 @@ window.__ModuleLoader__.load({
108
109
  "usage.bank.save": "保存图片",
109
110
  "usage.bank.share": "分享",
110
111
  "usage.bank.drawError": "票券生成失败:{error}",
111
- "usage.sidebar.entry.label": "用量",
112
- "usage.sidebar.entry.tooltip": "用量面板:套餐配额与余额",
113
- "usage.sidebar.toggle.collapse": "折叠",
114
- "usage.sidebar.toggle.expand": "展开",
115
- "usage.sidebar.empty": "没有已配置的套餐或余额数据。",
116
- "usage.sidebar.error": "加载失败:{error}",
117
- "usage.sidebar.loading": "正在加载用量数据…",
118
- "usage.sidebar.balanceLeft": "剩余 {balance}",
119
- "usage.sidebar.todayUsage": "今日 {tokens} tokens"
112
+ "usage.foot.cost": "今日消费",
113
+ "usage.foot.noData": "今日暂无用量",
114
+ "usage.foot.open": "打开使用统计设置",
115
+ "usage.foot.collapse": "收起用量卡片",
116
+ "usage.foot.expand": "展开用量卡片"
120
117
  };
121
118
  /** English mirror; every zh key present. */
122
119
  const en = {
@@ -159,6 +156,7 @@ window.__ModuleLoader__.load({
159
156
  "usage.config.title": "Settings",
160
157
  "usage.config.enabled": "Enable plugin",
161
158
  "usage.config.pollIntervalSec": "Poll interval (seconds)",
159
+ "usage.config.saveFailed": "Save failed: the deployment did not accept the setting.",
162
160
  "usage.bank.title": "Whale-yuan voucher",
163
161
  "usage.bank.hint": "Every 1,000,000 tokens spent on the official API mint one whale yuan; save or share the note.",
164
162
  "usage.bank.noUsage": "No official DeepSeek usage yet (counting starts when the plugin is enabled)",
@@ -169,15 +167,11 @@ window.__ModuleLoader__.load({
169
167
  "usage.bank.save": "Save image",
170
168
  "usage.bank.share": "Share",
171
169
  "usage.bank.drawError": "Failed to render the voucher: {error}",
172
- "usage.sidebar.entry.label": "Usage",
173
- "usage.sidebar.entry.tooltip": "Usage panel: plan quotas and balances",
174
- "usage.sidebar.toggle.collapse": "Collapse",
175
- "usage.sidebar.toggle.expand": "Expand",
176
- "usage.sidebar.empty": "No plan or balance data configured.",
177
- "usage.sidebar.error": "Loading failed: {error}",
178
- "usage.sidebar.loading": "Loading usage data…",
179
- "usage.sidebar.balanceLeft": "{balance} left",
180
- "usage.sidebar.todayUsage": "{tokens} tokens today"
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"
181
175
  };
182
176
  /**
183
177
  * Active dictionary, picked by the document language at call time. The
@@ -195,7 +189,7 @@ window.__ModuleLoader__.load({
195
189
  }
196
190
  //#endregion
197
191
  //#region \0dsh-css:packages/dsh-usage/src/client/usage.module.css.mjs
198
- 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_entry{box-sizing:border-box;width:100%;height:36px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;align-items:center;gap:8px;padding:0 10px;font-size:13px;display:flex}.cvtkAW_entry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.cvtkAW_entryIcon{flex:none;justify-content:center;align-items:center;width:24px;height:24px;display:inline-flex}.cvtkAW_entryIcon svg{width:18px;height:18px;display:block}.cvtkAW_entryLabel{text-overflow:ellipsis;overflow:hidden}.cvtkAW_entryMain{appearance:none;min-width:0;height:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;flex:1;align-items:center;gap:8px;padding:0;display:flex}.cvtkAW_entryAction{appearance:none;width:22px;height:22px;color:inherit;cursor:pointer;opacity:.55;background:0 0;border:none;border-radius:6px;flex:none;justify-content:center;align-items:center;padding:0;transition:opacity .12s,background-color .12s;display:inline-flex}.cvtkAW_entry:hover .cvtkAW_entryAction{opacity:.8}.cvtkAW_entryAction:hover{opacity:1;background:color-mix(in srgb, currentColor 10%, transparent)}.cvtkAW_entryAction:focus-visible,.cvtkAW_entryMain:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_entryAction:disabled{cursor:default;opacity:.4}.cvtkAW_entryAction svg{width:14px;height:14px;display:block}.cvtkAW_entryAction:disabled svg{animation:.9s linear infinite cvtkAW_usageEntrySpin}@keyframes cvtkAW_usageEntrySpin{to{transform:rotate(360deg)}}[data-dsh-frame][data-sidebar-collapsed] .cvtkAW_entry,[data-sidebar-collapsed] .cvtkAW_entry{border-radius:50%;justify-content:center;width:36px;height:36px;margin:0 auto 12px;padding:0}[data-dsh-frame][data-sidebar-collapsed] .cvtkAW_entryMain,[data-sidebar-collapsed] .cvtkAW_entryMain{flex:none;justify-content:center;width:100%}[data-dsh-frame][data-sidebar-collapsed] .cvtkAW_entryAction,[data-sidebar-collapsed] .cvtkAW_entryAction{display:none}[data-dsh-frame][data-sidebar-collapsed] .cvtkAW_sidebarSummary,[data-sidebar-collapsed] .cvtkAW_sidebarSummary{display:none}[data-dsh-frame][data-sidebar-collapsed] .cvtkAW_entryLabel,[data-sidebar-collapsed] .cvtkAW_entryLabel{display:none}.cvtkAW_sidebarSummary{color:inherit;opacity:.75;white-space:nowrap;align-items:baseline;gap:6px;margin:0 4px 12px;padding:0 10px;font-size:11px;display:flex;overflow:hidden}.cvtkAW_sidebarSummaryName{text-overflow:ellipsis;flex:none;max-width:40%;font-weight:600;overflow:hidden}.cvtkAW_sidebarSummaryFacts{text-overflow:ellipsis;font-variant-numeric:tabular-nums;overflow:hidden}.cvtkAW_sidebarPanel{border:1px solid color-mix(in srgb, currentColor 14%, transparent);color:inherit;background:color-mix(in srgb, currentColor 4%, transparent);border-radius:10px;flex-direction:column;gap:8px;margin:0 4px 12px;padding:10px 10px 12px;display:flex}.cvtkAW_sidebarProvider{flex-direction:column;gap:4px;display:flex}.cvtkAW_sidebarProviderHead{justify-content:space-between;align-items:baseline;gap:8px;font-size:12px;display:flex}.cvtkAW_sidebarProviderName{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.cvtkAW_sidebarBalance{opacity:.75;flex:none}.cvtkAW_sidebarWindowRow{opacity:.85;align-items:center;gap:6px;font-size:11px;display:flex}.cvtkAW_sidebarBar{background:color-mix(in srgb, currentColor 14%, transparent);border-radius:2px;flex:1;height:4px;overflow:hidden}.cvtkAW_sidebarBarFill{background:currentColor;border-radius:2px;height:100%;display:block}.cvtkAW_sidebarMuted{opacity:.6;font-size:11px}";
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}";
199
193
  const tagId = "@linxin666/dsh-usage/packages/dsh-usage/src/client/usage.module.css";
200
194
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
201
195
  const tag = document.createElement("style");
@@ -224,12 +218,20 @@ window.__ModuleLoader__.load({
224
218
  "chartTokens": "cvtkAW_chartTokens",
225
219
  "currentBadge": "cvtkAW_currentBadge",
226
220
  "currentProvider": "cvtkAW_currentProvider",
227
- "entry": "cvtkAW_entry",
228
- "entryAction": "cvtkAW_entryAction",
229
- "entryIcon": "cvtkAW_entryIcon",
230
- "entryLabel": "cvtkAW_entryLabel",
231
- "entryMain": "cvtkAW_entryMain",
232
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",
233
235
  "header": "cvtkAW_header",
234
236
  "muted": "cvtkAW_muted",
235
237
  "planCard": "cvtkAW_planCard",
@@ -244,18 +246,6 @@ window.__ModuleLoader__.load({
244
246
  "section": "cvtkAW_section",
245
247
  "settingItem": "cvtkAW_settingItem",
246
248
  "settingsGrid": "cvtkAW_settingsGrid",
247
- "sidebarBalance": "cvtkAW_sidebarBalance",
248
- "sidebarBar": "cvtkAW_sidebarBar",
249
- "sidebarBarFill": "cvtkAW_sidebarBarFill",
250
- "sidebarMuted": "cvtkAW_sidebarMuted",
251
- "sidebarPanel": "cvtkAW_sidebarPanel",
252
- "sidebarProvider": "cvtkAW_sidebarProvider",
253
- "sidebarProviderHead": "cvtkAW_sidebarProviderHead",
254
- "sidebarProviderName": "cvtkAW_sidebarProviderName",
255
- "sidebarSummary": "cvtkAW_sidebarSummary",
256
- "sidebarSummaryFacts": "cvtkAW_sidebarSummaryFacts",
257
- "sidebarSummaryName": "cvtkAW_sidebarSummaryName",
258
- "sidebarWindowRow": "cvtkAW_sidebarWindowRow",
259
249
  "stat": "cvtkAW_stat",
260
250
  "statLabel": "cvtkAW_statLabel",
261
251
  "statRow": "cvtkAW_statRow",
@@ -264,356 +254,11 @@ window.__ModuleLoader__.load({
264
254
  "tabActive": "cvtkAW_tabActive",
265
255
  "tabs": "cvtkAW_tabs",
266
256
  "trendAxis": "cvtkAW_trendAxis",
267
- "usageEntrySpin": "cvtkAW_usageEntrySpin",
268
257
  "voucherPreview": "cvtkAW_voucherPreview",
269
258
  "windowLabel": "cvtkAW_windowLabel",
270
259
  "windowRow": "cvtkAW_windowRow"
271
260
  };
272
261
  //#endregion
273
- //#region src/client/body-mutations.ts
274
- /** Cross-bundle registry key; `Symbol.for` so every module copy agrees. */
275
- const HUB_KEY = Symbol.for("dsh-web.body-mutation-hub");
276
- const INVALIDATION_ONLY = Symbol.for("dsh-web.body-mutation-invalidation");
277
- function needsRecords(subscribers) {
278
- for (const listener of subscribers) if (!listener[INVALIDATION_ONLY]) return true;
279
- return false;
280
- }
281
- /**
282
- * Subscribe to a coalesced DOM re-check without retaining mutation records.
283
- * The marked wrapper also works with an older hub, which delivers records
284
- * that it simply ignores until a page reload picks up the updated hub.
285
- */
286
- function subscribeBodyInvalidations(subscriber) {
287
- const listener = () => {
288
- subscriber();
289
- };
290
- listener[INVALIDATION_ONLY] = true;
291
- return subscribeBodyMutations(listener);
292
- }
293
- /**
294
- * Subscribe to body-level childList mutations.
295
- * @param subscriber - called at most once per animation frame with the records
296
- * collected since the previous flush; must be safe to run repeatedly.
297
- * @returns the disposer removing this subscriber (and the observer when it was
298
- * the last one).
299
- */
300
- function subscribeBodyMutations(subscriber) {
301
- if (typeof globalThis === "undefined" || typeof document === "undefined") return () => {};
302
- if (typeof MutationObserver !== "function") return () => {};
303
- const registry = globalThis;
304
- let hub = registry[HUB_KEY];
305
- if (hub === void 0) {
306
- const subscribers = /* @__PURE__ */ new Set();
307
- const created = {
308
- observer: void 0,
309
- subscribers,
310
- pending: [],
311
- scheduled: false
312
- };
313
- const flush = () => {
314
- created.frame = void 0;
315
- created.scheduled = false;
316
- const batch = created.pending;
317
- created.pending = [];
318
- for (const listener of [...subscribers]) {
319
- if (!subscribers.has(listener)) continue;
320
- try {
321
- listener(batch);
322
- } catch {}
323
- }
324
- };
325
- const schedule = () => {
326
- if (created.scheduled) return;
327
- created.scheduled = true;
328
- if (typeof requestAnimationFrame === "function") created.frame = requestAnimationFrame(flush);
329
- else flush();
330
- };
331
- created.observer = new MutationObserver((records) => {
332
- if (needsRecords(subscribers)) for (const record of records) created.pending.push(record);
333
- schedule();
334
- });
335
- created.observer.observe(document.body ?? document.documentElement, {
336
- childList: true,
337
- subtree: true
338
- });
339
- registry[HUB_KEY] = created;
340
- hub = created;
341
- }
342
- const active = hub;
343
- active.subscribers.add(subscriber);
344
- let subscribed = true;
345
- return () => {
346
- if (!subscribed) return;
347
- subscribed = false;
348
- active.subscribers.delete(subscriber);
349
- if (!needsRecords(active.subscribers)) active.pending = [];
350
- if (active.subscribers.size === 0 && registry[HUB_KEY] === active) {
351
- active.observer.disconnect();
352
- if (active.frame !== void 0 && typeof cancelAnimationFrame === "function") cancelAnimationFrame(active.frame);
353
- active.frame = void 0;
354
- active.pending = [];
355
- active.scheduled = false;
356
- delete registry[HUB_KEY];
357
- }
358
- };
359
- }
360
- //#endregion
361
- //#region src/client/sidebar-entry-core.ts
362
- /**
363
- * Shared sidebar entry injection core.
364
- *
365
- * dsh's sidebar shell exposes no slot an external plugin can register into,
366
- * so the entry row is injected between the shell's New Session button and the
367
- * workspace browser. The injection self-heals: a MutationObserver watches the
368
- * sidebar root and re-inserts the row whenever a React re-render displaces it
369
- * (re-insertion happens in the same frame, before paint, so no flicker).
370
- *
371
- * The row is plain DOM (no React tree) so it can never disturb the shell's
372
- * reconciliation; the view it toggles is a separate root owned by the caller.
373
- *
374
- * Packages receive this file as a generated copy via scripts/sync-shared.mjs;
375
- * edit the shared source and re-run the sync instead of editing a copy.
376
- */
377
- /** Find the sidebar shell root element, or undefined while not yet mounted. */
378
- function sidebarRoot() {
379
- const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]");
380
- if (column === null) return void 0;
381
- return column.querySelector("[class*=\"logoRow\"]")?.parentElement ?? column.firstElementChild;
382
- }
383
- /** The New Session button: nested in the logo row on current shells, a direct child on legacy shells. */
384
- function newSessionButton(root) {
385
- const nested = root.querySelector("button[class*=\"newSession\"]");
386
- if (nested !== null) return nested;
387
- for (const child of root.children) if (child.tagName === "BUTTON") return child;
388
- }
389
- /** Build the entry row (detached; inserted once the shell is up). */
390
- function createEntry(options) {
391
- const actions = options.actions ?? [];
392
- const composite = actions.length > 0;
393
- const entry = document.createElement(composite ? "div" : "button");
394
- if (!composite) entry.type = "button";
395
- entry.setAttribute(options.rowAttribute, "");
396
- if (options.plugin !== void 0) {
397
- entry.setAttribute("data-dsh-plugin", options.plugin);
398
- entry.setAttribute("data-dsh-part", "sidebar-entry");
399
- }
400
- entry.className = options.css["entry"] ?? "";
401
- const labelSpan = document.createElement("span");
402
- labelSpan.className = options.css["entryLabel"] ?? "";
403
- const iconSpan = document.createElement("span");
404
- iconSpan.className = options.css["entryIcon"] ?? "";
405
- iconSpan.innerHTML = options.icon;
406
- const main = composite ? document.createElement("button") : entry;
407
- if (composite) {
408
- main.type = "button";
409
- main.className = options.css["entryMain"] ?? "";
410
- main.append(iconSpan, labelSpan);
411
- entry.append(main);
412
- } else entry.append(iconSpan, labelSpan);
413
- const actionButtons = [];
414
- for (const action of actions) {
415
- const button = document.createElement("button");
416
- button.type = "button";
417
- button.className = options.css["entryAction"] ?? "";
418
- button.setAttribute("data-dsh-entry-action", action.id);
419
- button.innerHTML = action.icon;
420
- button.addEventListener("click", () => {
421
- action.onClick(button);
422
- });
423
- entry.append(button);
424
- actionButtons.push({
425
- action,
426
- button
427
- });
428
- }
429
- let open = false;
430
- const applyActions = () => {
431
- for (const { action, button } of actionButtons) {
432
- button.innerHTML = open || action.inactiveIcon === void 0 ? action.icon : action.inactiveIcon;
433
- const text = action.label(open);
434
- button.setAttribute("aria-label", text);
435
- button.setAttribute("title", text);
436
- if (action.inactiveIcon !== void 0) button.setAttribute("aria-expanded", String(open));
437
- }
438
- };
439
- const applyLabel = () => {
440
- main.setAttribute("aria-label", options.label());
441
- if (options.tooltip !== void 0) main.setAttribute("title", options.tooltip());
442
- labelSpan.textContent = options.label();
443
- applyActions();
444
- };
445
- applyLabel();
446
- main.addEventListener("click", options.onToggle);
447
- if (composite) entry.addEventListener("click", (event) => {
448
- if (event.target === entry) options.onToggle();
449
- });
450
- return {
451
- entry,
452
- applyLabel,
453
- setOpen: (next) => {
454
- open = next;
455
- applyActions();
456
- }
457
- };
458
- }
459
- /** Re-insert the entry after the New Session row (before the browser region). */
460
- function placeEntry(root, entry, options) {
461
- const button = newSessionButton(root);
462
- if (button === void 0) return false;
463
- if (entry.parentElement !== root) {
464
- const row = button.closest("[class*=\"logoRow\"]");
465
- const base = row !== null && row.parentElement === root ? row : button;
466
- const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(options.familySelectors.join(", ")));
467
- const anchor = options.position === "before" ? family.length > 0 ? family[0] : base.nextElementSibling : family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling;
468
- root.insertBefore(entry, anchor);
469
- }
470
- return true;
471
- }
472
- /**
473
- * Mount the sidebar entry, waiting for the shell to render and self-healing
474
- * on later React re-renders.
475
- * @param options - the row's attribute/icon/copy/action/ordering configuration.
476
- * @returns disposer removing the entry and its observers.
477
- */
478
- function mountSidebarEntry$1(options) {
479
- if (typeof document !== "undefined" && document.querySelector(options.rowSelector) !== null) return () => {};
480
- const { entry, applyLabel, setOpen } = createEntry(options);
481
- let root;
482
- let placed = false;
483
- let unsubscribeRefresh;
484
- if (options.refresh !== void 0) try {
485
- unsubscribeRefresh = options.refresh.subscribe(applyLabel);
486
- } catch {}
487
- const tryPlace = () => {
488
- if (root !== void 0 && !root.isConnected) {
489
- rootObserver.disconnect();
490
- root = void 0;
491
- placed = false;
492
- }
493
- if (placed) {
494
- if (document.body.contains(entry)) return;
495
- rootObserver.disconnect();
496
- root = void 0;
497
- placed = false;
498
- }
499
- root ??= sidebarRoot();
500
- if (root === void 0) return;
501
- placed = placeEntry(root, entry, options);
502
- if (placed) rootObserver.observe(root, {
503
- childList: true,
504
- subtree: true
505
- });
506
- };
507
- const unsubscribeBody = subscribeBodyInvalidations(() => {
508
- tryPlace();
509
- });
510
- const rootObserver = new MutationObserver(() => {
511
- if (root === void 0 || !root.isConnected) {
512
- placed = false;
513
- tryPlace();
514
- return;
515
- }
516
- if (!root.contains(entry)) placed = placeEntry(root, entry, options);
517
- });
518
- const unsubscribeActive = options.active === void 0 ? void 0 : (() => {
519
- const syncActive = () => {
520
- const open = options.active.isOpen();
521
- if (open) entry.dataset.active = "true";
522
- else delete entry.dataset.active;
523
- setOpen(open);
524
- };
525
- const unsubscribe = options.active.subscribe(syncActive);
526
- syncActive();
527
- return unsubscribe;
528
- })();
529
- tryPlace();
530
- return () => {
531
- unsubscribeBody();
532
- rootObserver.disconnect();
533
- unsubscribeRefresh?.();
534
- unsubscribeActive?.();
535
- entry.remove();
536
- };
537
- }
538
- //#endregion
539
- //#region src/client/sidebar-entry.ts
540
- /**
541
- * Sidebar entry injection for the usage panel (issue #1592) — wiring over the
542
- * shared core. Unlike the panel-takeover family (task board / SSH / skill
543
- * center) this row does not participate in the family block, so it is placed
544
- * directly under the New Session row and ordered after those entries. The row
545
- * seats the panel's controls as trailing actions: a refresh command and a
546
- * collapse chevron mirroring the open state, so the panel below renders only
547
- * its content.
548
- * @module @linxin666/dsh-usage/client/sidebar-entry
549
- */
550
- /** Stable data attribute identifying the injected entry row. */
551
- const ENTRY_SELECTOR = "[data-dsh-usage-entry]";
552
- /** Inline gauge glyph normalized to the shell's 18px navigation glyph size. */
553
- const ICON = "<svg viewBox=\"0 0 16 16\" width=\"18\" height=\"18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M2.5 12.5a7 7 0 0 1 11 -4.3\"/><path d=\"M8 12.5V8.2l2.9-2.1\"/><circle cx=\"8\" cy=\"12.5\" r=\"1\"/></svg>";
554
- /** Refresh glyph: a clockwise circular arrow, 14px like the sibling action icons. */
555
- const REFRESH_ICON = "<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M13.4 8a5.4 5.4 0 1 1-1.58-3.82\"/><path d=\"M13.4 2.6v2.7h-2.7\"/></svg>";
556
- /** Disclosure chevrons: down while expanded, right while collapsed. */
557
- const CHEVRON_OPEN_ICON = "<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m4 6 4 4 4-4\"/></svg>";
558
- const CHEVRON_CLOSED_ICON = "<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m6 4 4 4-4 4\"/></svg>";
559
- /** Refresh cooldown: one forced probe cycle per click, the icon spins meanwhile. */
560
- const REFRESH_COOLDOWN_MS = 3e3;
561
- /** Trailing actions seated at the row's right edge. */
562
- function entryActions(handlers) {
563
- return [{
564
- id: "refresh",
565
- icon: REFRESH_ICON,
566
- label: () => t("usage.refresh"),
567
- onClick: (button) => {
568
- handlers.onRefresh();
569
- button.disabled = true;
570
- window.setTimeout(() => {
571
- button.disabled = false;
572
- }, REFRESH_COOLDOWN_MS);
573
- }
574
- }, {
575
- id: "collapse",
576
- icon: CHEVRON_OPEN_ICON,
577
- inactiveIcon: CHEVRON_CLOSED_ICON,
578
- label: (open) => t(open ? "usage.sidebar.toggle.collapse" : "usage.sidebar.toggle.expand"),
579
- onClick: () => {
580
- handlers.onToggle();
581
- }
582
- }];
583
- }
584
- /**
585
- * Mount the usage sidebar entry.
586
- * @param handlers - the panel's toggle/refresh callbacks and open-state face.
587
- * @param locale - locale-change source; when given, re-applies the label on a
588
- * language switch (the plain-DOM row otherwise keeps the mount-time copy).
589
- * @returns disposer removing the entry and its observers.
590
- */
591
- function mountSidebarEntry(handlers, locale) {
592
- return mountSidebarEntry$1({
593
- rowAttribute: "data-dsh-usage-entry",
594
- rowSelector: ENTRY_SELECTOR,
595
- plugin: "usage",
596
- icon: ICON,
597
- css: usage_module_css_default,
598
- label: () => t("usage.sidebar.entry.label"),
599
- tooltip: () => t("usage.sidebar.entry.tooltip"),
600
- refresh: locale === void 0 ? void 0 : { subscribe: (listener) => locale.subscribe(listener) },
601
- onToggle: handlers.onToggle,
602
- actions: entryActions(handlers),
603
- position: "after",
604
- familySelectors: [
605
- "[data-dsh-taskboard-entry]",
606
- "[data-dsh-ssh-entry]",
607
- "[data-dsh-skill-explorer-entry]",
608
- "[data-dsh-usage-entry]"
609
- ],
610
- active: {
611
- subscribe: handlers.subscribeOpen,
612
- isOpen: handlers.isOpen
613
- }
614
- });
615
- }
616
- //#endregion
617
262
  //#region src/core/adapters.ts
618
263
  /** Parse a string/number into a finite number, else undefined. */
619
264
  function toNum(value) {
@@ -1104,31 +749,6 @@ window.__ModuleLoader__.load({
1104
749
  };
1105
750
  }
1106
751
  //#endregion
1107
- //#region src/core/types.ts
1108
- /** A zeroed totals bucket. */
1109
- function emptyTotals() {
1110
- return {
1111
- inputTokens: 0,
1112
- outputTokens: 0,
1113
- cacheReadTokens: 0,
1114
- cacheWriteTokens: 0,
1115
- reasoningTokens: 0,
1116
- calls: 0,
1117
- cost: 0
1118
- };
1119
- }
1120
- /** Add `right` into `left` in place. */
1121
- function addTotals(left, right) {
1122
- left.inputTokens += right.inputTokens;
1123
- left.outputTokens += right.outputTokens;
1124
- left.cacheReadTokens += right.cacheReadTokens;
1125
- left.cacheWriteTokens += right.cacheWriteTokens;
1126
- left.reasoningTokens += right.reasoningTokens;
1127
- left.calls += right.calls;
1128
- left.cost += right.cost;
1129
- return left;
1130
- }
1131
- //#endregion
1132
752
  //#region src/core/ledger.ts
1133
753
  /** Total tokens of a bucket (billed input + output; reasoning is inside output). */
1134
754
  function totalTokens(totals) {
@@ -1291,7 +911,7 @@ window.__ModuleLoader__.load({
1291
911
  return "";
1292
912
  }
1293
913
  }
1294
- function formatClock(ms) {
914
+ function formatClock$1(ms) {
1295
915
  try {
1296
916
  return new Date(ms).toLocaleTimeString([], {
1297
917
  hour: "2-digit",
@@ -1382,7 +1002,7 @@ window.__ModuleLoader__.load({
1382
1002
  ]
1383
1003
  });
1384
1004
  }
1385
- function balanceLine(provider) {
1005
+ function balanceLine$1(provider) {
1386
1006
  if (provider.balance !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1387
1007
  className: usage_module_css_default.providerBalance,
1388
1008
  children: [
@@ -1418,7 +1038,7 @@ window.__ModuleLoader__.load({
1418
1038
  })]
1419
1039
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1420
1040
  className: usage_module_css_default.providerTokens,
1421
- children: balanceLine(provider)
1041
+ children: balanceLine$1(provider)
1422
1042
  })]
1423
1043
  });
1424
1044
  }
@@ -1554,7 +1174,7 @@ window.__ModuleLoader__.load({
1554
1174
  deepseekVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1555
1175
  className: usage_module_css_default.muted,
1556
1176
  "data-dsh-part": "peak-status",
1557
- 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) })
1558
1178
  }),
1559
1179
  snapshot.usage.today.totals.calls === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1560
1180
  className: usage_module_css_default.muted,
@@ -1936,158 +1556,362 @@ window.__ModuleLoader__.load({
1936
1556
  ]
1937
1557
  });
1938
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
+ */
1939
1565
  function SettingsRow(props) {
1940
1566
  const { settings, snapshot, value } = props;
1941
1567
  const disabled = snapshot === void 0 || !snapshot.writable;
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
+ };
1942
1584
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1943
1585
  className: usage_module_css_default.card,
1944
1586
  "data-dsh-part": "settings-row",
1945
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1946
- className: usage_module_css_default.cardTitle,
1947
- children: t("usage.config.title")
1948
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1949
- className: usage_module_css_default.settingsGrid,
1950
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1951
- className: usage_module_css_default.settingItem,
1952
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1953
- type: "checkbox",
1954
- checked: value.enabled ?? true,
1955
- disabled,
1956
- onChange: (event) => {
1957
- settings.set("enabled", event.target.checked);
1958
- }
1959
- }), t("usage.config.enabled")]
1960
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1961
- className: usage_module_css_default.settingItem,
1962
- children: [t("usage.config.pollIntervalSec"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1963
- type: "number",
1964
- min: 30,
1965
- max: 3600,
1966
- value: typeof value.pollIntervalSec === "number" ? value.pollIntervalSec : 60,
1967
- disabled,
1968
- onChange: (event) => {
1969
- const parsed = Number(event.target.value);
1970
- if (Number.isFinite(parsed) && parsed >= 30 && parsed <= 3600) settings.set("pollIntervalSec", Math.round(parsed));
1971
- }
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", {
1595
+ className: usage_module_css_default.settingItem,
1596
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1597
+ type: "checkbox",
1598
+ checked: value.enabled ?? true,
1599
+ disabled,
1600
+ onChange: (event) => {
1601
+ write("enabled", event.target.checked);
1602
+ }
1603
+ }), t("usage.config.enabled")]
1604
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1605
+ className: usage_module_css_default.settingItem,
1606
+ children: [t("usage.config.pollIntervalSec"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1607
+ type: "number",
1608
+ min: 30,
1609
+ max: 3600,
1610
+ value: typeof value.pollIntervalSec === "number" ? value.pollIntervalSec : 60,
1611
+ disabled,
1612
+ onChange: (event) => {
1613
+ const parsed = Number(event.target.value);
1614
+ if (Number.isFinite(parsed) && parsed >= 30 && parsed <= 3600) write("pollIntervalSec", Math.round(parsed));
1615
+ }
1616
+ })]
1972
1617
  })]
1973
- })]
1974
- })]
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
+ ]
1975
1625
  });
1976
1626
  }
1977
1627
  //#endregion
1978
- //#region src/client/UsageSidebarPanel.tsx
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
+ }
1979
1636
  /**
1980
- * Sidebar usage panel (issue #1592): the usage body seated directly under the
1981
- * sidebar entry row. The row carries the panel's controls (refresh, collapse
1982
- * chevron), so this component renders only the content: expanded, plan
1983
- * providers show their quota windows and balance-only providers their
1984
- * remaining amount; collapsed, a one-line strip shows the current session
1985
- * provider's today usage (tokens, calls, and cost when priced) from the same
1986
- * /api/dsh-usage/overview document the settings section renders. The open
1987
- * state lives in the mount (persisted to localStorage) and arrives through
1988
- * the open face; polling runs at 10s expanded, 30s collapsed, and pauses
1989
- * while the page is hidden.
1990
- * @module @linxin666/dsh-usage/client/UsageSidebarPanel
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.
1991
1640
  */
1992
- /** Poll cadence while the panel is expanded and the page visible. */
1993
- const PANEL_POLL_MS = 1e4;
1641
+ function subscribeBodyInvalidations(subscriber) {
1642
+ const listener = () => {
1643
+ subscriber();
1644
+ };
1645
+ listener[INVALIDATION_ONLY] = true;
1646
+ return subscribeBodyMutations(listener);
1647
+ }
1994
1648
  /**
1995
- * Poll cadence of the collapsed strip: the current session's usage should
1996
- * stay visibly live, but a collapsed surface pays a relaxed cadence instead
1997
- * of the expanded one.
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).
1998
1654
  */
1999
- const COLLAPSED_POLL_MS = 3e4;
2000
- /** Bar tone, matching the settings section's thresholds. */
2001
- function tone(percent) {
2002
- if (percent >= 90) return usage_module_css_default.barLow;
2003
- if (percent >= 70) return usage_module_css_default.barWarn;
2004
- return usage_module_css_default.sidebarBarFill;
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
+ };
2005
1714
  }
1715
+ //#endregion
1716
+ //#region src/client/UsageFootCard.tsx
2006
1717
  /**
2007
- * The collapsed-strip facts for the current provider: display name plus
2008
- * today's adapter-family totals (tokens, calls, cost). The host serves them
2009
- * strip-ready inside `current`; an older host document is derived against
2010
- * here. Undefined on a day without usage — a strip about nothing is noise.
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
2011
1740
  */
2012
- function collapsedSummary(snapshot) {
2013
- const provider = snapshot.current.provider;
2014
- if (provider === void 0) return void 0;
2015
- const name = snapshot.current.displayName ?? snapshot.providers.find((entry) => entry.provider === provider)?.displayName ?? provider;
2016
- if (snapshot.current.today !== void 0) {
2017
- const totals = snapshot.current.today;
2018
- return totals.calls > 0 && totalTokens(totals) > 0 ? {
2019
- name,
2020
- totals
2021
- } : void 0;
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;
2022
1753
  }
2023
- const family = adapterFor(provider);
2024
- let merged = emptyTotals();
2025
- for (const row of snapshot.usage.today.providers) if (family === void 0 ? row.provider === provider : adapterFor(row.provider) === family) merged = addTotals(merged, row.totals);
2026
- return merged.calls > 0 && totalTokens(merged) > 0 ? {
2027
- name,
2028
- totals: merged
2029
- } : void 0;
2030
- }
2031
- /** A provider is worth listing when it is configured and carries a fact. */
2032
- function listable(provider) {
2033
- return provider.credential !== "none" && (provider.plan !== void 0 || provider.balance !== void 0);
2034
- }
2035
- /** One provider row: plan windows when present, otherwise the balance line. */
2036
- function ProviderBlock(props) {
2037
- const { provider } = props;
2038
- const windows = provider.plan?.windows ?? [];
2039
- if (windows.length > 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2040
- className: usage_module_css_default.sidebarProvider,
2041
- "data-dsh-part": "sidebar-provider",
2042
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2043
- className: usage_module_css_default.sidebarProviderHead,
2044
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2045
- className: usage_module_css_default.sidebarProviderName,
2046
- children: provider.displayName
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"
2047
1780
  })
2048
- }), windows.map((window) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2049
- className: usage_module_css_default.sidebarWindowRow,
2050
- "data-dsh-part": "sidebar-window",
2051
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: window.name ?? t(`usage.plan.windows.${window.key}`) }), window.percent !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2052
- className: usage_module_css_default.sidebarBar,
2053
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2054
- className: tone(window.percent),
2055
- style: { width: `${Math.min(100, Math.max(0, window.percent))}%` }
2056
- })
2057
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [window.percent >= 10 ? Math.round(window.percent) : window.percent.toFixed(1), "%"] })] })]
2058
- }, window.key))]
1781
+ ]
2059
1782
  });
2060
- if (provider.balance !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2061
- className: usage_module_css_default.sidebarProvider,
2062
- "data-dsh-part": "sidebar-provider",
2063
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2064
- className: usage_module_css_default.sidebarProviderHead,
2065
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2066
- className: usage_module_css_default.sidebarProviderName,
2067
- children: provider.displayName
2068
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2069
- className: usage_module_css_default.sidebarBalance,
2070
- children: t("usage.sidebar.balanceLeft", { balance: `${provider.balance.currency} ${provider.balance.totalBalance}` })
2071
- })]
2072
- })
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" })
2073
1808
  });
2074
- return null;
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
+ }
2075
1831
  }
2076
1832
  /**
2077
- * Render the sidebar usage panel body.
2078
- * @param props - store, the poll callback, and the mount's open-state face.
2079
- * @returns the panel content, or null while collapsed.
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.
2080
1837
  */
2081
- function UsageSidebarPanel(props) {
2082
- const { store, poll, open: openState } = props;
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;
2083
1897
  const ui = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot);
2084
- const open = (0, react.useSyncExternalStore)(openState.subscribe, openState.isOpen);
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
+ };
2085
1909
  (0, react.useEffect)(() => {
1910
+ if (!enabled) return void 0;
2086
1911
  poll();
2087
- const cadence = open ? PANEL_POLL_MS : COLLAPSED_POLL_MS;
2088
1912
  let timer;
2089
1913
  const start = () => {
2090
- if (timer === void 0 && document.visibilityState === "visible") timer = window.setInterval(poll, cadence);
1914
+ if (timer === void 0 && document.visibilityState === "visible") timer = window.setInterval(poll, FOOT_CARD_POLL_MS);
2091
1915
  };
2092
1916
  const onVisibility = () => {
2093
1917
  if (document.visibilityState === "visible") {
@@ -2104,128 +1928,194 @@ window.__ModuleLoader__.load({
2104
1928
  if (timer !== void 0) window.clearInterval(timer);
2105
1929
  document.removeEventListener("visibilitychange", onVisibility);
2106
1930
  };
2107
- }, [poll, open]);
1931
+ }, [poll, enabled]);
1932
+ if (!enabled) return null;
2108
1933
  const snapshot = ui.snapshot;
2109
- if (!open) {
2110
- const summary = snapshot === null ? void 0 : collapsedSummary(snapshot);
2111
- if (summary === void 0) return null;
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");
2112
1946
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2113
- className: usage_module_css_default.sidebarSummary,
1947
+ className: cardClass(collapsed),
2114
1948
  "data-dsh-plugin": "usage",
2115
- "data-dsh-part": "sidebar-summary",
2116
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2117
- className: usage_module_css_default.sidebarSummaryName,
2118
- children: summary.name
2119
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2120
- className: usage_module_css_default.sidebarSummaryFacts,
2121
- children: [
2122
- t("usage.sidebar.todayUsage", { tokens: formatTokens(totalTokens(summary.totals)) }),
2123
- " · ",
2124
- t("usage.calls", { n: summary.totals.calls }),
2125
- summary.totals.cost > 0 ? ` · ¥${summary.totals.cost.toFixed(2)}` : ""
2126
- ]
2127
- })]
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]
2128
1975
  });
2129
1976
  }
2130
- const providers = snapshot === null ? [] : snapshot.providers.filter(listable);
2131
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2132
- className: usage_module_css_default.sidebarPanel,
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),
2133
1981
  "data-dsh-plugin": "usage",
2134
- "data-dsh-part": "sidebar-panel",
2135
- children: ui.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2136
- className: usage_module_css_default.sidebarMuted,
2137
- children: t("usage.sidebar.error", { error: ui.error ?? "" })
2138
- }) : snapshot === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2139
- className: usage_module_css_default.sidebarMuted,
2140
- children: t("usage.sidebar.loading")
2141
- }) : providers.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2142
- className: usage_module_css_default.sidebarMuted,
2143
- children: t("usage.sidebar.empty")
2144
- }) : providers.map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderBlock, { provider }, provider.provider))
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
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) })
2046
+ })
2047
+ ]
2048
+ }), toggle]
2145
2049
  });
2146
2050
  }
2147
- //#endregion
2148
- //#region src/client/sidebar-panel-mount.tsx
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
+ }
2149
2055
  /**
2150
- * Sidebar usage panel mounting (issue #1592).
2151
- *
2152
- * The panel is a plain DOM row inserted into the sidebar right below the
2153
- * usage entry row, carrying its own React root. Placement self-heals with the
2154
- * same page-wide body-mutation hub the entry row uses; the entry row stays the
2155
- * anchor, so the sidebar order never depends on this module's timing.
2156
- *
2157
- * The mount owns the panel's single open state: initialized from localStorage,
2158
- * flipped by the entry row's toggle controls, and mirrored to the React body
2159
- * through the subscribe/isOpen face (the same face the entry row's chevron
2160
- * and highlight read).
2161
- * @module @linxin666/dsh-usage/client/sidebar-panel-mount
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.
2162
2063
  */
2163
- /** localStorage key holding the collapsed flag ('1' = collapsed). */
2164
- const COLLAPSED_STORAGE_KEY = "dsh-usage.sidebar.collapsed";
2165
- /** Read the persisted collapsed flag (absent = expanded). */
2166
- function readCollapsed() {
2167
- try {
2168
- return window.localStorage.getItem(COLLAPSED_STORAGE_KEY) === "1";
2169
- } catch {
2170
- return false;
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;
2171
2077
  }
2172
- }
2173
- /** Persist the collapsed flag; storage failures keep the session state. */
2174
- function writeCollapsed(collapsed) {
2175
- try {
2176
- window.localStorage.setItem(COLLAPSED_STORAGE_KEY, collapsed ? "1" : "0");
2177
- } catch {}
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);
2178
2090
  }
2179
2091
  /**
2180
- * Mount the sidebar usage panel below its entry row.
2181
- * @param props - the store and poll callback of the apply body.
2182
- * @returns the panel controller.
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.
2183
2095
  */
2184
- function mountUsagePanel(props) {
2096
+ function mountUsageFootCard(props) {
2097
+ if (typeof document !== "undefined" && document.querySelector("[data-dsh-usage-foot-card]") !== null) return () => {};
2185
2098
  const container = document.createElement("div");
2186
- container.setAttribute("data-dsh-usage-view", "");
2187
- const listeners = /* @__PURE__ */ new Set();
2188
- let open = !readCollapsed();
2189
- const emit = () => {
2190
- for (const listener of [...listeners]) listener();
2191
- };
2192
- const openState = {
2193
- subscribe: (listener) => {
2194
- listeners.add(listener);
2195
- return () => {
2196
- listeners.delete(listener);
2197
- };
2198
- },
2199
- isOpen: () => open
2200
- };
2099
+ container.setAttribute("data-dsh-usage-foot-card", "");
2201
2100
  const root = (0, react_dom_client.createRoot)(container);
2202
- root.render((0, react.createElement)(UsageSidebarPanel, {
2203
- store: props.store,
2204
- poll: props.poll,
2205
- open: openState
2206
- }));
2207
- /** Keep the panel as the row directly after the usage entry. */
2101
+ root.render((0, react.createElement)(UsageFootCard, props));
2102
+ /** Keep the container directly above the Settings seat inside the foot area. */
2208
2103
  const place = () => {
2209
- const entry = document.querySelector(ENTRY_SELECTOR);
2210
- if (entry === null || entry.parentElement === null) return;
2211
- if (container.parentElement !== entry.parentElement || entry.nextElementSibling !== container) entry.parentElement.insertBefore(container, entry.nextElementSibling);
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);
2212
2112
  };
2213
2113
  place();
2214
2114
  const unsubscribeBody = subscribeBodyInvalidations(place);
2215
- return {
2216
- isOpen: () => open,
2217
- subscribe: openState.subscribe,
2218
- toggle: () => {
2219
- open = !open;
2220
- writeCollapsed(!open);
2221
- emit();
2222
- },
2223
- dispose: () => {
2224
- unsubscribeBody();
2225
- root.unmount();
2226
- container.remove();
2227
- listeners.clear();
2228
- }
2115
+ return () => {
2116
+ unsubscribeBody();
2117
+ root.unmount();
2118
+ container.remove();
2229
2119
  };
2230
2120
  }
2231
2121
  //#endregion
@@ -2244,7 +2134,7 @@ window.__ModuleLoader__.load({
2244
2134
  overview: () => usageFetch("/api/dsh-usage/overview", "GET"),
2245
2135
  refresh: () => usageFetch("/api/dsh-usage/refresh", "POST")
2246
2136
  };
2247
- /** 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). */
2248
2138
  const USAGE_SETTINGS_NS = "dsh-usage";
2249
2139
  /** First-level nav position: directly below the Workshop section (order 150). */
2250
2140
  const SECTION_ORDER = 151;
@@ -2253,7 +2143,7 @@ window.__ModuleLoader__.load({
2253
2143
  "slots",
2254
2144
  "locale",
2255
2145
  "connection",
2256
- "settingsScope",
2146
+ "configForms",
2257
2147
  "remote"
2258
2148
  ];
2259
2149
  /**
@@ -2273,7 +2163,8 @@ window.__ModuleLoader__.load({
2273
2163
  return () => {};
2274
2164
  }
2275
2165
  }, "dsh-usage: dictionaries");
2276
- 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);
2277
2168
  const store = createUsageStore().create();
2278
2169
  let pollSeq = 0;
2279
2170
  const poll = () => {
@@ -2302,28 +2193,22 @@ window.__ModuleLoader__.load({
2302
2193
  store,
2303
2194
  poll,
2304
2195
  refresh,
2305
- settings: settingsScope
2196
+ settings: settingsForm
2306
2197
  });
2307
- const panel = mountUsagePanel({
2198
+ const disposeFootCard = mountUsageFootCard({
2308
2199
  store,
2309
- poll
2310
- });
2311
- const disposeEntry = mountSidebarEntry({
2312
- onToggle: () => {
2313
- panel.toggle();
2314
- },
2315
- onRefresh: () => {
2316
- refresh();
2200
+ poll,
2201
+ settings: settingsForm,
2202
+ onOpen: () => {
2203
+ openUsageSettings(() => t("usage.title"));
2317
2204
  },
2318
- isOpen: () => panel.isOpen(),
2319
- subscribeOpen: (listener) => panel.subscribe(listener)
2320
- }, ctx.locale);
2205
+ locale: ctx.locale
2206
+ });
2321
2207
  ctx.effect(() => () => {
2322
2208
  try {
2323
- disposeEntry();
2209
+ disposeFootCard();
2324
2210
  } catch {}
2325
- panel.dispose();
2326
- }, "dsh-usage: sidebar panel");
2211
+ }, "dsh-usage: sidebar foot card");
2327
2212
  ctx.slots.inject("settings.section", () => {
2328
2213
  try {
2329
2214
  const unregister = ctx.slots.register({