@linxin666/dsh-usage 0.3.22 → 0.3.24
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/README.i18n.yaml +2 -2
- package/README.md +6 -8
- package/README.zh.md +6 -8
- package/lib/client.js +749 -78
- package/lib/client.js.map +1 -1
- package/lib/index.js +171 -201
- package/lib/types/client/UsageSectionCard.d.ts +0 -1
- package/lib/types/client/UsageSidebarPanel.d.ts +34 -0
- package/lib/types/client/body-mutations.d.ts +45 -0
- package/lib/types/client/locales.d.ts +9 -4
- package/lib/types/client/sidebar-entry-core.d.ts +87 -0
- package/lib/types/client/sidebar-entry.d.ts +35 -0
- package/lib/types/client/sidebar-panel-mount.d.ts +40 -0
- package/lib/types/client/voucher.d.ts +2 -2
- package/lib/types/core/provider-routes.d.ts +26 -0
- package/lib/types/core/types.d.ts +14 -1
- package/lib/types/host/access.d.ts +17 -0
- package/lib/types/host/pair-access.d.ts +34 -0
- package/lib/types/host/routes.d.ts +16 -7
- package/lib/types/host/usage-service.d.ts +28 -73
- package/lib/types/index.d.ts +0 -2
- package/package.json +4 -3
- package/src/client/UsageSectionCard.tsx +1 -15
- package/src/client/UsageSidebarPanel.tsx +196 -0
- package/src/client/body-mutations.ts +133 -0
- package/src/client/index.ts +22 -1
- package/src/client/locales.ts +20 -10
- package/src/client/sidebar-entry-core.ts +321 -0
- package/src/client/sidebar-entry.ts +101 -0
- package/src/client/sidebar-panel-mount.tsx +113 -0
- package/src/client/usage.module.css +252 -0
- package/src/client/voucher.ts +2 -2
- package/src/core/adapters.ts +40 -6
- package/src/core/provider-routes.ts +37 -0
- package/src/core/types.ts +14 -1
- package/src/host/access.ts +21 -0
- package/src/host/pair-access.ts +49 -0
- package/src/host/routes.ts +19 -10
- package/src/host/usage-service.ts +65 -214
- package/src/index.ts +1 -6
package/lib/client.js
CHANGED
|
@@ -5,6 +5,7 @@ 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");
|
|
8
9
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
10
|
//#region \0dsh-store-engine
|
|
10
11
|
const platform = ["@deepseek-ai/dsh-client", "-store"].join("");
|
|
@@ -97,12 +98,8 @@ 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": "关闭",
|
|
104
101
|
"usage.bank.title": "鲸元券",
|
|
105
|
-
"usage.bank.hint": "官方 API 每消耗
|
|
102
|
+
"usage.bank.hint": "官方 API 每消耗 100 万 tokens 铸造 1 鲸元;保存或分享这张票券。",
|
|
106
103
|
"usage.bank.noUsage": "暂无 DeepSeek 官方用量数据(统计自插件启用起)",
|
|
107
104
|
"usage.bank.minted": "累计铸造 {minted} 鲸元({tokens} tokens)",
|
|
108
105
|
"usage.bank.spend.observed": "官方余额实测花费 ¥{cost}(自 {since} 起)",
|
|
@@ -110,7 +107,16 @@ window.__ModuleLoader__.load({
|
|
|
110
107
|
"usage.bank.window": "统计窗口 {from} ~ {to}",
|
|
111
108
|
"usage.bank.save": "保存图片",
|
|
112
109
|
"usage.bank.share": "分享",
|
|
113
|
-
"usage.bank.drawError": "票券生成失败:{error}"
|
|
110
|
+
"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"
|
|
114
120
|
};
|
|
115
121
|
/** English mirror; every zh key present. */
|
|
116
122
|
const en = {
|
|
@@ -153,12 +159,8 @@ window.__ModuleLoader__.load({
|
|
|
153
159
|
"usage.config.title": "Settings",
|
|
154
160
|
"usage.config.enabled": "Enable plugin",
|
|
155
161
|
"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",
|
|
160
162
|
"usage.bank.title": "Whale-yuan voucher",
|
|
161
|
-
"usage.bank.hint": "Every 1,000 tokens spent on the official API mint one whale yuan; save or share the note.",
|
|
163
|
+
"usage.bank.hint": "Every 1,000,000 tokens spent on the official API mint one whale yuan; save or share the note.",
|
|
162
164
|
"usage.bank.noUsage": "No official DeepSeek usage yet (counting starts when the plugin is enabled)",
|
|
163
165
|
"usage.bank.minted": "Minted {minted} whale yuan ({tokens} tokens)",
|
|
164
166
|
"usage.bank.spend.observed": "Spent ¥{cost} observed on the official balance (watching since {since})",
|
|
@@ -166,7 +168,16 @@ window.__ModuleLoader__.load({
|
|
|
166
168
|
"usage.bank.window": "Window {from} - {to}",
|
|
167
169
|
"usage.bank.save": "Save image",
|
|
168
170
|
"usage.bank.share": "Share",
|
|
169
|
-
"usage.bank.drawError": "Failed to render the voucher: {error}"
|
|
171
|
+
"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
181
|
};
|
|
171
182
|
/**
|
|
172
183
|
* Active dictionary, picked by the document language at call time. The
|
|
@@ -184,7 +195,7 @@ window.__ModuleLoader__.load({
|
|
|
184
195
|
}
|
|
185
196
|
//#endregion
|
|
186
197
|
//#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}";
|
|
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}";
|
|
188
199
|
const tagId = "@linxin666/dsh-usage/packages/dsh-usage/src/client/usage.module.css";
|
|
189
200
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
190
201
|
const tag = document.createElement("style");
|
|
@@ -213,6 +224,11 @@ window.__ModuleLoader__.load({
|
|
|
213
224
|
"chartTokens": "cvtkAW_chartTokens",
|
|
214
225
|
"currentBadge": "cvtkAW_currentBadge",
|
|
215
226
|
"currentProvider": "cvtkAW_currentProvider",
|
|
227
|
+
"entry": "cvtkAW_entry",
|
|
228
|
+
"entryAction": "cvtkAW_entryAction",
|
|
229
|
+
"entryIcon": "cvtkAW_entryIcon",
|
|
230
|
+
"entryLabel": "cvtkAW_entryLabel",
|
|
231
|
+
"entryMain": "cvtkAW_entryMain",
|
|
216
232
|
"errorLine": "cvtkAW_errorLine",
|
|
217
233
|
"header": "cvtkAW_header",
|
|
218
234
|
"muted": "cvtkAW_muted",
|
|
@@ -228,6 +244,18 @@ window.__ModuleLoader__.load({
|
|
|
228
244
|
"section": "cvtkAW_section",
|
|
229
245
|
"settingItem": "cvtkAW_settingItem",
|
|
230
246
|
"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",
|
|
231
259
|
"stat": "cvtkAW_stat",
|
|
232
260
|
"statLabel": "cvtkAW_statLabel",
|
|
233
261
|
"statRow": "cvtkAW_statRow",
|
|
@@ -236,11 +264,356 @@ window.__ModuleLoader__.load({
|
|
|
236
264
|
"tabActive": "cvtkAW_tabActive",
|
|
237
265
|
"tabs": "cvtkAW_tabs",
|
|
238
266
|
"trendAxis": "cvtkAW_trendAxis",
|
|
267
|
+
"usageEntrySpin": "cvtkAW_usageEntrySpin",
|
|
239
268
|
"voucherPreview": "cvtkAW_voucherPreview",
|
|
240
269
|
"windowLabel": "cvtkAW_windowLabel",
|
|
241
270
|
"windowRow": "cvtkAW_windowRow"
|
|
242
271
|
};
|
|
243
272
|
//#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
|
|
244
617
|
//#region src/core/adapters.ts
|
|
245
618
|
/** Parse a string/number into a finite number, else undefined. */
|
|
246
619
|
function toNum(value) {
|
|
@@ -254,6 +627,10 @@ window.__ModuleLoader__.load({
|
|
|
254
627
|
function str(value) {
|
|
255
628
|
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
256
629
|
}
|
|
630
|
+
/** Clamp a provider percentage into the renderable 0-100 band. */
|
|
631
|
+
function clampPercent(value) {
|
|
632
|
+
return Math.max(0, Math.min(100, value));
|
|
633
|
+
}
|
|
257
634
|
/** Format a number to a fixed 2-decimal display string. */
|
|
258
635
|
function money(value) {
|
|
259
636
|
return value.toFixed(2);
|
|
@@ -386,7 +763,28 @@ window.__ModuleLoader__.load({
|
|
|
386
763
|
}
|
|
387
764
|
}
|
|
388
765
|
};
|
|
389
|
-
/**
|
|
766
|
+
/**
|
|
767
|
+
* GLM Coding Plan quota windows, keyed by the provider's own `unit` code
|
|
768
|
+
* rather than by row position so an added or reordered limit cannot shift a
|
|
769
|
+
* window's meaning: 3 = the rolling 5-hour window, 6 = the weekly window,
|
|
770
|
+
* 5 = the monthly one. Two row kinds carry quota — `TOKENS_LIMIT` (token
|
|
771
|
+
* plans) and `CREDIT_LIMIT` (credit plans, whose exact ratio is
|
|
772
|
+
* `currentValue` / `usage`) — while `TIME_LIMIT` is the MCP request cap and
|
|
773
|
+
* is not a plan window at all. Auth is the RAW key without a Bearer prefix.
|
|
774
|
+
*/
|
|
775
|
+
const GLM_UNIT_KEYS = {
|
|
776
|
+
3: "5h",
|
|
777
|
+
5: "month",
|
|
778
|
+
6: "week"
|
|
779
|
+
};
|
|
780
|
+
/** Percent from a credit row's exact ratio, else the provider's own percentage. */
|
|
781
|
+
function glmCreditPercent(row) {
|
|
782
|
+
const used = toNum(row.currentValue);
|
|
783
|
+
const limit = toNum(row.usage);
|
|
784
|
+
if (used !== void 0 && limit !== void 0 && limit > 0) return clampPercent(used / limit * 100);
|
|
785
|
+
const percentage = toNum(row.percentage);
|
|
786
|
+
return percentage === void 0 ? void 0 : clampPercent(percentage);
|
|
787
|
+
}
|
|
390
788
|
function glmPlan(host, ids) {
|
|
391
789
|
return {
|
|
392
790
|
ids,
|
|
@@ -411,11 +809,17 @@ window.__ModuleLoader__.load({
|
|
|
411
809
|
for (const entry of limits) {
|
|
412
810
|
if (typeof entry !== "object" || entry === null) continue;
|
|
413
811
|
const row = entry;
|
|
414
|
-
const
|
|
415
|
-
|
|
812
|
+
const kind = str(row.type);
|
|
813
|
+
if (kind === "TIME_LIMIT") continue;
|
|
814
|
+
const key = GLM_UNIT_KEYS[toNum(row.unit) ?? NaN];
|
|
815
|
+
if (key === void 0) continue;
|
|
816
|
+
if (kind !== "TOKENS_LIMIT" && kind !== "CREDIT_LIMIT") continue;
|
|
416
817
|
windows.push({
|
|
417
|
-
key
|
|
418
|
-
percent:
|
|
818
|
+
key,
|
|
819
|
+
percent: kind === "CREDIT_LIMIT" ? glmCreditPercent(row) : (() => {
|
|
820
|
+
const percentage = toNum(row.percentage);
|
|
821
|
+
return percentage === void 0 ? void 0 : clampPercent(percentage);
|
|
822
|
+
})(),
|
|
419
823
|
resetsAt: toIso(row.nextResetTime)
|
|
420
824
|
});
|
|
421
825
|
}
|
|
@@ -629,7 +1033,7 @@ window.__ModuleLoader__.load({
|
|
|
629
1033
|
moonshotBalance("api.moonshot.ai", "USD", ["moonshotai"]),
|
|
630
1034
|
KIMI_CODING,
|
|
631
1035
|
glmPlan("open.bigmodel.cn", ["zai-coding-cn"]),
|
|
632
|
-
glmPlan("api.z.ai", ["zai-coding"]),
|
|
1036
|
+
glmPlan("api.z.ai", ["zai", "zai-coding"]),
|
|
633
1037
|
OPENCODE_GO,
|
|
634
1038
|
minimaxPlan("api.minimaxi.com", ["minimax-cn"]),
|
|
635
1039
|
minimaxPlan("api.minimax.io", ["minimax"]),
|
|
@@ -700,6 +1104,31 @@ window.__ModuleLoader__.load({
|
|
|
700
1104
|
};
|
|
701
1105
|
}
|
|
702
1106
|
//#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
|
|
703
1132
|
//#region src/core/ledger.ts
|
|
704
1133
|
/** Total tokens of a bucket (billed input + output; reasoning is inside output). */
|
|
705
1134
|
function totalTokens(totals) {
|
|
@@ -728,8 +1157,8 @@ window.__ModuleLoader__.load({
|
|
|
728
1157
|
* section renders as its failure line.
|
|
729
1158
|
* @module @linxin666/dsh-usage/client/voucher
|
|
730
1159
|
*/
|
|
731
|
-
/** Anti-inflation exchange rate:
|
|
732
|
-
const TOKENS_PER_WHALE_YUAN =
|
|
1160
|
+
/** Anti-inflation exchange rate: 1,000,000 tokens mint one whale yuan. */
|
|
1161
|
+
const TOKENS_PER_WHALE_YUAN = 1e6;
|
|
733
1162
|
/** The note's face value in whale yuan; the smallest denomination is 1. */
|
|
734
1163
|
function faceValue(tokens) {
|
|
735
1164
|
return Math.max(1, Math.round(tokens / TOKENS_PER_WHALE_YUAN));
|
|
@@ -1320,7 +1749,7 @@ window.__ModuleLoader__.load({
|
|
|
1320
1749
|
}
|
|
1321
1750
|
/**
|
|
1322
1751
|
* The Token 银行 card: the DeepSeek official family's retained-ledger usage
|
|
1323
|
-
* minted onto the whale-yuan note at
|
|
1752
|
+
* minted onto the whale-yuan note at 1,000,000 tokens per whale yuan. The window
|
|
1324
1753
|
* prefers the host's whole-ledger aggregate and falls back to the 30-day
|
|
1325
1754
|
* trend when an older host serves no `all`; the spend line prefers the
|
|
1326
1755
|
* official balance watch and falls back to the fold-time estimate; the
|
|
@@ -1510,11 +1939,6 @@ window.__ModuleLoader__.load({
|
|
|
1510
1939
|
function SettingsRow(props) {
|
|
1511
1940
|
const { settings, snapshot, value } = props;
|
|
1512
1941
|
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";
|
|
1518
1942
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1519
1943
|
className: usage_module_css_default.card,
|
|
1520
1944
|
"data-dsh-part": "settings-row",
|
|
@@ -1523,61 +1947,288 @@ window.__ModuleLoader__.load({
|
|
|
1523
1947
|
children: t("usage.config.title")
|
|
1524
1948
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1525
1949
|
className: usage_module_css_default.settingsGrid,
|
|
1526
|
-
children: [
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
})]
|
|
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
|
-
]
|
|
1574
|
-
})]
|
|
1575
|
-
})
|
|
1576
|
-
]
|
|
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
|
+
}
|
|
1972
|
+
})]
|
|
1973
|
+
})]
|
|
1577
1974
|
})]
|
|
1578
1975
|
});
|
|
1579
1976
|
}
|
|
1580
1977
|
//#endregion
|
|
1978
|
+
//#region src/client/UsageSidebarPanel.tsx
|
|
1979
|
+
/**
|
|
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
|
|
1991
|
+
*/
|
|
1992
|
+
/** Poll cadence while the panel is expanded and the page visible. */
|
|
1993
|
+
const PANEL_POLL_MS = 1e4;
|
|
1994
|
+
/**
|
|
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.
|
|
1998
|
+
*/
|
|
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;
|
|
2005
|
+
}
|
|
2006
|
+
/**
|
|
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.
|
|
2011
|
+
*/
|
|
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;
|
|
2022
|
+
}
|
|
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
|
|
2047
|
+
})
|
|
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))]
|
|
2059
|
+
});
|
|
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
|
+
})
|
|
2073
|
+
});
|
|
2074
|
+
return null;
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
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.
|
|
2080
|
+
*/
|
|
2081
|
+
function UsageSidebarPanel(props) {
|
|
2082
|
+
const { store, poll, open: openState } = props;
|
|
2083
|
+
const ui = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot);
|
|
2084
|
+
const open = (0, react.useSyncExternalStore)(openState.subscribe, openState.isOpen);
|
|
2085
|
+
(0, react.useEffect)(() => {
|
|
2086
|
+
poll();
|
|
2087
|
+
const cadence = open ? PANEL_POLL_MS : COLLAPSED_POLL_MS;
|
|
2088
|
+
let timer;
|
|
2089
|
+
const start = () => {
|
|
2090
|
+
if (timer === void 0 && document.visibilityState === "visible") timer = window.setInterval(poll, cadence);
|
|
2091
|
+
};
|
|
2092
|
+
const onVisibility = () => {
|
|
2093
|
+
if (document.visibilityState === "visible") {
|
|
2094
|
+
poll();
|
|
2095
|
+
start();
|
|
2096
|
+
} else if (timer !== void 0) {
|
|
2097
|
+
window.clearInterval(timer);
|
|
2098
|
+
timer = void 0;
|
|
2099
|
+
}
|
|
2100
|
+
};
|
|
2101
|
+
start();
|
|
2102
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
2103
|
+
return () => {
|
|
2104
|
+
if (timer !== void 0) window.clearInterval(timer);
|
|
2105
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
2106
|
+
};
|
|
2107
|
+
}, [poll, open]);
|
|
2108
|
+
const snapshot = ui.snapshot;
|
|
2109
|
+
if (!open) {
|
|
2110
|
+
const summary = snapshot === null ? void 0 : collapsedSummary(snapshot);
|
|
2111
|
+
if (summary === void 0) return null;
|
|
2112
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2113
|
+
className: usage_module_css_default.sidebarSummary,
|
|
2114
|
+
"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
|
+
})]
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
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,
|
|
2133
|
+
"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))
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
//#endregion
|
|
2148
|
+
//#region src/client/sidebar-panel-mount.tsx
|
|
2149
|
+
/**
|
|
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
|
|
2162
|
+
*/
|
|
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;
|
|
2171
|
+
}
|
|
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 {}
|
|
2178
|
+
}
|
|
2179
|
+
/**
|
|
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.
|
|
2183
|
+
*/
|
|
2184
|
+
function mountUsagePanel(props) {
|
|
2185
|
+
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
|
+
};
|
|
2201
|
+
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. */
|
|
2208
|
+
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);
|
|
2212
|
+
};
|
|
2213
|
+
place();
|
|
2214
|
+
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
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
//#endregion
|
|
1581
2232
|
//#region src/client/index.ts
|
|
1582
2233
|
/** Hard ceiling for one usage API call; a stalled host must not pile up requests. */
|
|
1583
2234
|
const USAGE_FETCH_TIMEOUT_MS = 2e4;
|
|
@@ -1653,6 +2304,26 @@ window.__ModuleLoader__.load({
|
|
|
1653
2304
|
refresh,
|
|
1654
2305
|
settings: settingsScope
|
|
1655
2306
|
});
|
|
2307
|
+
const panel = mountUsagePanel({
|
|
2308
|
+
store,
|
|
2309
|
+
poll
|
|
2310
|
+
});
|
|
2311
|
+
const disposeEntry = mountSidebarEntry({
|
|
2312
|
+
onToggle: () => {
|
|
2313
|
+
panel.toggle();
|
|
2314
|
+
},
|
|
2315
|
+
onRefresh: () => {
|
|
2316
|
+
refresh();
|
|
2317
|
+
},
|
|
2318
|
+
isOpen: () => panel.isOpen(),
|
|
2319
|
+
subscribeOpen: (listener) => panel.subscribe(listener)
|
|
2320
|
+
}, ctx.locale);
|
|
2321
|
+
ctx.effect(() => () => {
|
|
2322
|
+
try {
|
|
2323
|
+
disposeEntry();
|
|
2324
|
+
} catch {}
|
|
2325
|
+
panel.dispose();
|
|
2326
|
+
}, "dsh-usage: sidebar panel");
|
|
1656
2327
|
ctx.slots.inject("settings.section", () => {
|
|
1657
2328
|
try {
|
|
1658
2329
|
const unregister = ctx.slots.register({
|