@gamegeek-saikel/dsh-cost-meter 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +116 -0
  3. package/README.zh-CN.md +116 -0
  4. package/cordis.patch.yml +12 -0
  5. package/lib/client.js +1481 -0
  6. package/lib/client.js.map +1 -0
  7. package/lib/index.js +2403 -0
  8. package/lib/invariant.js +38 -0
  9. package/lib/types/client/AssistantCostChip.d.ts +29 -0
  10. package/lib/types/client/AssistantCostChip.d.ts.map +1 -0
  11. package/lib/types/client/AssistantCostChip.js +75 -0
  12. package/lib/types/client/CostPluginCard.d.ts +10 -0
  13. package/lib/types/client/CostPluginCard.d.ts.map +1 -0
  14. package/lib/types/client/CostPluginCard.js +124 -0
  15. package/lib/types/client/CostView.d.ts +10 -0
  16. package/lib/types/client/CostView.d.ts.map +1 -0
  17. package/lib/types/client/CostView.js +66 -0
  18. package/lib/types/client/SessionCostLine.d.ts +21 -0
  19. package/lib/types/client/SessionCostLine.d.ts.map +1 -0
  20. package/lib/types/client/SessionCostLine.js +173 -0
  21. package/lib/types/client/SessionCostPill.d.ts +13 -0
  22. package/lib/types/client/SessionCostPill.d.ts.map +1 -0
  23. package/lib/types/client/SessionCostPill.js +123 -0
  24. package/lib/types/client/cost-math.d.ts +88 -0
  25. package/lib/types/client/cost-math.d.ts.map +1 -0
  26. package/lib/types/client/cost-math.js +199 -0
  27. package/lib/types/client/format.d.ts +35 -0
  28. package/lib/types/client/format.d.ts.map +1 -0
  29. package/lib/types/client/format.js +61 -0
  30. package/lib/types/client/index.d.ts +28 -0
  31. package/lib/types/client/index.d.ts.map +1 -0
  32. package/lib/types/client/index.js +59 -0
  33. package/lib/types/client/locales.d.ts +78 -0
  34. package/lib/types/client/locales.d.ts.map +1 -0
  35. package/lib/types/client/locales.js +149 -0
  36. package/lib/types/index.d.ts +136 -0
  37. package/lib/types/index.d.ts.map +1 -0
  38. package/lib/types/index.js +401 -0
  39. package/lib/types/invariant.d.ts +16 -0
  40. package/lib/types/invariant.d.ts.map +1 -0
  41. package/lib/types/invariant.js +38 -0
  42. package/lib/types/pricebook.d.ts +467 -0
  43. package/lib/types/pricebook.d.ts.map +1 -0
  44. package/lib/types/pricebook.js +696 -0
  45. package/lib/types/pricing.d.ts +93 -0
  46. package/lib/types/pricing.d.ts.map +1 -0
  47. package/lib/types/pricing.js +346 -0
  48. package/lib/types/session-cost-projection.d.ts +70 -0
  49. package/lib/types/session-cost-projection.d.ts.map +1 -0
  50. package/lib/types/session-cost-projection.js +198 -0
  51. package/lib/types/subagent-cost.d.ts +51 -0
  52. package/lib/types/subagent-cost.d.ts.map +1 -0
  53. package/lib/types/subagent-cost.js +79 -0
  54. package/lib/types/types.d.ts +334 -0
  55. package/lib/types/types.d.ts.map +1 -0
  56. package/lib/types/types.js +6 -0
  57. package/package.json +134 -0
  58. package/src/client/AssistantCostChip.module.css +81 -0
  59. package/src/client/AssistantCostChip.tsx +109 -0
  60. package/src/client/CostPluginCard.module.css +186 -0
  61. package/src/client/CostPluginCard.tsx +264 -0
  62. package/src/client/CostView.module.css +168 -0
  63. package/src/client/CostView.tsx +145 -0
  64. package/src/client/SessionCostLine.module.css +19 -0
  65. package/src/client/SessionCostLine.tsx +186 -0
  66. package/src/client/SessionCostPill.module.css +118 -0
  67. package/src/client/SessionCostPill.tsx +192 -0
  68. package/src/client/cost-math.ts +230 -0
  69. package/src/client/format.ts +63 -0
  70. package/src/client/index.ts +108 -0
  71. package/src/client/locales.ts +165 -0
  72. package/src/css-modules.d.ts +4 -0
  73. package/src/index.ts +489 -0
  74. package/src/invariant.ts +47 -0
  75. package/src/pricebook.ts +825 -0
  76. package/src/pricing.ts +378 -0
  77. package/src/session-cost-projection.ts +238 -0
  78. package/src/subagent-cost.ts +105 -0
  79. package/src/types.ts +364 -0
package/lib/client.js ADDED
@@ -0,0 +1,1481 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@gamegeek-saikel/dsh-cost-meter",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ //#region src/client/locales.ts
11
+ /** `cost-meter` namespace dictionaries. */
12
+ /** Dictionary namespace owned by this plugin. */
13
+ const NS = "cost-meter";
14
+ /** Simplified Chinese dictionary (the key-set source of truth). */
15
+ const zh = {
16
+ "view.cost": "花费",
17
+ "view.empty": "暂无花费数据",
18
+ "view.total": "总花费",
19
+ "view.uncached": "输入(缓存未命中)",
20
+ "view.cacheRead": "输入(缓存命中)",
21
+ "view.output": "输出",
22
+ "view.cacheWriteTokens": "缓存写入",
23
+ "view.unpriced": "{count} 步无价格",
24
+ "view.steps": "每轮回复花费",
25
+ "step.turn": "第 {turn} 轮",
26
+ "step.model": "模型 {model}",
27
+ "step.time": "{time}",
28
+ "step.tokens": "未命中 {uncached} · 缓存读 {read} · 缓存写 {write} · 输出 {output}",
29
+ "step.cost": "花费 {amount}",
30
+ "step.snapshot": "快照 · {time}",
31
+ "step.unpriced": "—({reason})",
32
+ "reason.NO_MODEL": "无模型信息",
33
+ "reason.NO_PRICE": "无价格",
34
+ "cost.label": "本会话 {amount}",
35
+ "line.uncached": "非缓存输入 {amount}",
36
+ "line.cacheRead": "缓存输入 {amount}",
37
+ "line.output": "输出 {amount}",
38
+ "line.subagent": "子代理 {amount}",
39
+ "pill.label": "花费 {amount}",
40
+ "pill.estimate": "预计 {amount}",
41
+ "pill.estimateNote": "估算",
42
+ "pill.detail.total": "总花费 {amount}",
43
+ "pill.detail.snapshot": "价格快照({time})",
44
+ "pill.detail.model": "模型 {model}",
45
+ "pill.detail.source": "价格来源 {source}",
46
+ "source.official": "官方页面",
47
+ "source.fallback": "内置回退",
48
+ "source.manual": "手动",
49
+ "source.openrouter": "OpenRouter",
50
+ "source.none": "无",
51
+ "band.peak": "高峰价",
52
+ "band.offPeak": "闲时价",
53
+ "band.single": "单价",
54
+ "price.peakExtra": "高峰 · 比闲时多 {multiplier} 倍",
55
+ "price.offPeakSaving": "闲时 · 比高峰省 {percent}",
56
+ "price.peakRatio": "高峰 {multiplier}",
57
+ "price.offPeakRatio": "闲时 {multiplier}",
58
+ "chip.title": "本回复花费 {amount}",
59
+ "chip.unpriced": "—",
60
+ "balance.label": "余额 {amount}",
61
+ "balance.failed": "余额不可用",
62
+ "balance.detail": "余额 {amount}(赠送 {granted} · 充值 {toppedUp})",
63
+ "balance.suspended": "账户已暂停付费请求",
64
+ "refreshedAt": "更新于 {time}",
65
+ "settings.title": "花费计价",
66
+ "settings.current": "当前快照 · {source} · {time}",
67
+ "settings.noSnapshot": "尚无价格快照",
68
+ "settings.refresh": "立即刷新",
69
+ "settings.refreshing": "刷新中…",
70
+ "settings.overrides": "按模型价目表(手动覆盖,元/百万 tokens)",
71
+ "settings.aliases": "OpenRouter 别名映射",
72
+ "settings.discount": "缓存折扣系数(OpenRouter)",
73
+ "settings.fx": "汇率(USD→CNY)",
74
+ "settings.fx.auto": "自动",
75
+ "settings.fx.manual": "手动",
76
+ "settings.balance": "显示余额",
77
+ "settings.openrouter": "允许 OpenRouter 回退",
78
+ "settings.save": "保存",
79
+ "settings.discard": "丢弃",
80
+ "settings.saved": "已保存",
81
+ "settings.error": "操作失败:{error}",
82
+ "settings.jsonHint": "JSON 对象,键为模型,值含 single/offPeak/peak 价格桶"
83
+ };
84
+ /** English dictionary, key-identical to the Chinese source of truth. */
85
+ const en = {
86
+ "view.cost": "Cost",
87
+ "view.empty": "No cost data yet",
88
+ "view.total": "Total",
89
+ "view.uncached": "Input (cache miss)",
90
+ "view.cacheRead": "Input (cache hit)",
91
+ "view.output": "Output",
92
+ "view.cacheWriteTokens": "Cache write",
93
+ "view.unpriced": "{count} steps unpriced",
94
+ "view.steps": "Per-reply cost",
95
+ "step.turn": "Turn {turn}",
96
+ "step.model": "Model {model}",
97
+ "step.time": "{time}",
98
+ "step.tokens": "miss {uncached} · read {read} · write {write} · out {output}",
99
+ "step.cost": "Cost {amount}",
100
+ "step.snapshot": "snapshot · {time}",
101
+ "step.unpriced": "—({reason})",
102
+ "reason.NO_MODEL": "no model info",
103
+ "reason.NO_PRICE": "no price",
104
+ "cost.label": "Session {amount}",
105
+ "line.uncached": "Uncached input {amount}",
106
+ "line.cacheRead": "Cached input {amount}",
107
+ "line.output": "Output {amount}",
108
+ "line.subagent": "Subagents {amount}",
109
+ "pill.label": "Cost {amount}",
110
+ "pill.estimate": "≈ {amount}",
111
+ "pill.estimateNote": "estimate",
112
+ "pill.detail.total": "Total {amount}",
113
+ "pill.detail.snapshot": "Price snapshot ({time})",
114
+ "pill.detail.model": "Model {model}",
115
+ "pill.detail.source": "Price source {source}",
116
+ "source.official": "official page",
117
+ "source.fallback": "built-in fallback",
118
+ "source.manual": "manual",
119
+ "source.openrouter": "OpenRouter",
120
+ "source.none": "none",
121
+ "band.peak": "peak",
122
+ "band.offPeak": "off-peak",
123
+ "band.single": "list",
124
+ "price.peakExtra": "peak · {multiplier}× more than off-peak",
125
+ "price.offPeakSaving": "off-peak · save {percent} vs peak",
126
+ "price.peakRatio": "peak {multiplier}",
127
+ "price.offPeakRatio": "off-peak {multiplier}",
128
+ "chip.title": "This reply cost {amount}",
129
+ "chip.unpriced": "—",
130
+ "balance.label": "Balance {amount}",
131
+ "balance.failed": "Balance unavailable",
132
+ "balance.detail": "Balance {amount} (granted {granted} · topped up {toppedUp})",
133
+ "balance.suspended": "Account suspended for paid requests",
134
+ "refreshedAt": "Updated at {time}",
135
+ "settings.title": "Cost meter",
136
+ "settings.current": "Current snapshot · {source} · {time}",
137
+ "settings.noSnapshot": "No price snapshot yet",
138
+ "settings.refresh": "Refresh now",
139
+ "settings.refreshing": "Refreshing…",
140
+ "settings.overrides": "Per-model prices (manual overrides, CNY per 1M tokens)",
141
+ "settings.aliases": "OpenRouter alias map",
142
+ "settings.discount": "Cache-read discount (OpenRouter)",
143
+ "settings.fx": "Exchange rate (USD→CNY)",
144
+ "settings.fx.auto": "Auto",
145
+ "settings.fx.manual": "Manual",
146
+ "settings.balance": "Show balance",
147
+ "settings.openrouter": "Allow OpenRouter fallback",
148
+ "settings.save": "Save",
149
+ "settings.discard": "Discard",
150
+ "settings.saved": "Saved",
151
+ "settings.error": "Operation failed: {error}",
152
+ "settings.jsonHint": "JSON object keyed by model; values carry single/offPeak/peak price buckets"
153
+ };
154
+ //#endregion
155
+ //#region src/pricing.ts
156
+ /** Official peak-pricing rollout: 2026-08-17 00:00 Beijing time (UTC+8). */
157
+ const PEAK_PRICING_START_MS = Date.UTC(2026, 7, 16, 16, 0, 0);
158
+ /** Built-in fallback USD prices from the English pricing page. */
159
+ const FALLBACK_CURRENT_USD = {
160
+ flash: {
161
+ cacheReadPerMillion: .007,
162
+ inputPerMillion: .22,
163
+ outputPerMillion: .66
164
+ },
165
+ pro: {
166
+ cacheReadPerMillion: .022,
167
+ inputPerMillion: .66,
168
+ outputPerMillion: 1.98
169
+ }
170
+ };
171
+ ({ ...FALLBACK_CURRENT_USD.flash }), { ...FALLBACK_CURRENT_USD.pro };
172
+ /**
173
+ * Whether the current moment is a peak-pricing hour in Beijing time:
174
+ * 09:00-12:00 and 14:00-18:00 (peak); everything else is off-peak.
175
+ * @param now - the instant to classify.
176
+ * @returns true during peak hours.
177
+ */
178
+ function isPeakHour(now = /* @__PURE__ */ new Date()) {
179
+ const parts = new Intl.DateTimeFormat("en-US", {
180
+ timeZone: "Asia/Shanghai",
181
+ hour: "numeric",
182
+ hour12: false
183
+ }).formatToParts(now);
184
+ const hour = Number(parts.find((part) => part.type === "hour")?.value);
185
+ if (Number.isNaN(hour)) return false;
186
+ return hour >= 9 && hour < 12 || hour >= 14 && hour < 18;
187
+ }
188
+ //#endregion
189
+ //#region src/client/cost-math.ts
190
+ /**
191
+ * Client-side price lookup and streaming-estimate helpers.
192
+ *
193
+ * The authoritative anchored ledger arrives through the `sessionCost`
194
+ * projection; this module only mirrors the host pricebook math for display
195
+ * and for the live estimate (which by nature uses CURRENT prices and is
196
+ * labeled 估算/estimate). The lookup logic intentionally mirrors
197
+ * `src/pricebook.ts` — keep the two in sync.
198
+ *
199
+ * @module @gamegeek-saikel/dsh-cost-meter/client/cost-math
200
+ */
201
+ /** Sum totals (this module's mirror of the host `sumTotals`). */
202
+ function sumTotals(totals) {
203
+ const sum = {
204
+ uncachedCost: 0,
205
+ cacheReadCost: 0,
206
+ outputCost: 0,
207
+ cost: 0,
208
+ pricedSteps: 0,
209
+ unpricedSteps: 0,
210
+ steps: 0
211
+ };
212
+ for (const entry of totals) {
213
+ sum.uncachedCost += entry.uncachedCost;
214
+ sum.cacheReadCost += entry.cacheReadCost;
215
+ sum.outputCost += entry.outputCost;
216
+ sum.cost += entry.cost;
217
+ sum.pricedSteps += entry.pricedSteps;
218
+ sum.unpricedSteps += entry.unpricedSteps;
219
+ sum.steps += entry.steps;
220
+ }
221
+ return sum;
222
+ }
223
+ /** Total spend of every subagent (CNY); 0 without subagents. */
224
+ function subagentSpend(subagents) {
225
+ if (subagents === void 0 || subagents.length === 0) return 0;
226
+ return sumTotals(subagents.map((entry) => entry.totals)).cost;
227
+ }
228
+ /**
229
+ * The conversation-wide totals: the main session's ledger plus every
230
+ * subagent's ledger (host-aggregated). Undefined while the main ledger is
231
+ * absent; identical to the main totals without subagents.
232
+ * @param main - the main session's projection totals.
233
+ * @param subagents - the host-aggregated subagent totals.
234
+ * @returns the combined totals, or undefined.
235
+ */
236
+ function combineTotals(main, subagents) {
237
+ if (main === void 0) return void 0;
238
+ if (subagents === void 0 || subagents.length === 0) return main;
239
+ return sumTotals([main, ...subagents.map((entry) => entry.totals)]);
240
+ }
241
+ /** The price band in effect at one instant (mirror of the host helper). */
242
+ function bandForTime(time) {
243
+ if (time < PEAK_PRICING_START_MS) return "single";
244
+ return isPeakHour(new Date(time)) ? "peak" : "offPeak";
245
+ }
246
+ /** The bucket of one model's entry for one band (mirror of the host helper). */
247
+ function bucketOf(modelPrice, band) {
248
+ if (band === "single") return modelPrice.single ?? modelPrice.offPeak ?? modelPrice.peak;
249
+ if (band === "peak") return modelPrice.peak ?? modelPrice.offPeak ?? modelPrice.single;
250
+ return modelPrice.offPeak ?? modelPrice.peak ?? modelPrice.single;
251
+ }
252
+ /** Map a model id to its official pricing key (mirror of the host helper). */
253
+ function pricingKeyOfModel(model) {
254
+ return model.toLowerCase().includes("pro") ? "pro" : "flash";
255
+ }
256
+ /** Candidate canonical price keys for one model (mirror of the host helper). */
257
+ function modelKeys(provider, model) {
258
+ const keys = [];
259
+ if (provider !== void 0 && provider.length > 0) keys.push(`${provider}/${model}`);
260
+ keys.push(model);
261
+ if (model.toLowerCase().includes("deepseek") || provider !== void 0 && provider.toLowerCase().includes("deepseek")) keys.push(pricingKeyOfModel(model));
262
+ return keys;
263
+ }
264
+ /** Resolve one model's price entry in a snapshot through the key chain. */
265
+ function modelPriceOf(snapshot, provider, model) {
266
+ if (snapshot === void 0 || snapshot === null) return void 0;
267
+ for (const key of modelKeys(provider, model)) {
268
+ const entry = snapshot.prices[key];
269
+ if (entry !== void 0) return entry;
270
+ }
271
+ }
272
+ /** Resolve one model's bucket at one instant from a snapshot. */
273
+ function bucketAt(snapshot, provider, model, time) {
274
+ const entry = modelPriceOf(snapshot, provider, model);
275
+ if (entry === void 0) return void 0;
276
+ return bucketOf(entry, bandForTime(time));
277
+ }
278
+ /**
279
+ * The peak-to-off-peak price multiplier for one model's entry, when the
280
+ * entry carries both bands. The multiplier is the ratio of the summed peak
281
+ * bucket rates to the summed off-peak bucket rates; official DeepSeek prices
282
+ * scale all buckets by the same factor, so this is a stable “peak is N× the
283
+ * off-peak price” display value.
284
+ * @param snapshot - the pricebook snapshot to read.
285
+ * @param provider - the provider route, when known.
286
+ * @param model - the model id.
287
+ * @returns the peak/off-peak multiplier, or null when either band is absent.
288
+ */
289
+ function peakOffPeakMultiplier(snapshot, provider, model) {
290
+ const entry = modelPriceOf(snapshot, provider, model);
291
+ if (entry === void 0) return null;
292
+ const peak = entry.peak;
293
+ const offPeak = entry.offPeak;
294
+ if (peak === void 0 || offPeak === void 0) return null;
295
+ const offPeakTotal = offPeak.cacheReadPerMillion + offPeak.inputPerMillion + offPeak.outputPerMillion;
296
+ if (offPeakTotal === 0) return null;
297
+ return (peak.cacheReadPerMillion + peak.inputPerMillion + peak.outputPerMillion) / offPeakTotal;
298
+ }
299
+ /** CJK characters (the /1.5 density class). */
300
+ const CJK_RE = /[\u3400-\u4dbf\u4e00-\u9fff]/g;
301
+ /**
302
+ * Heuristic character→token estimate: CJK ÷ 1.5, everything else ÷ 4
303
+ * (the streaming estimate's output side).
304
+ * @param text - the accumulated text.
305
+ * @returns the estimated token count.
306
+ */
307
+ function estimateTokens(text) {
308
+ const cjk = (text.match(CJK_RE) ?? []).length;
309
+ return Math.ceil(cjk / 1.5 + (text.length - cjk) / 4);
310
+ }
311
+ /**
312
+ * Tokens of the streamed text/reasoning blocks of a partial assistant.
313
+ * @param blocks - the partial's content blocks (text/reasoning carry `text`).
314
+ * @returns the estimated token count.
315
+ */
316
+ function blocksOutputTokens(blocks) {
317
+ let tokens = 0;
318
+ for (const block of blocks ?? []) if ((block.kind === "text" || block.kind === "reasoning") && typeof block.text === "string" && block.text.length > 0) tokens += estimateTokens(block.text);
319
+ return tokens;
320
+ }
321
+ /**
322
+ * The cache-read ratio of the newest completed step (input side of the
323
+ * estimate). Absent history conservatively prices all input as uncached
324
+ * (upper bound).
325
+ * @param steps - the projection's step ledger.
326
+ * @returns the ratio in [0, 1].
327
+ */
328
+ function cacheReadRatioOf(steps) {
329
+ for (let i = steps.length - 1; i >= 0; i -= 1) {
330
+ const step = steps[i];
331
+ const total = step.uncachedInputTokens + step.cacheReadTokens + step.cacheWriteTokens;
332
+ if (step.priced && total > 0) return step.cacheReadTokens / total;
333
+ }
334
+ return 0;
335
+ }
336
+ /**
337
+ * The live estimate of the running reply: input at the projected context
338
+ * size (weighted by the cache ratio) plus output at the streamed-token
339
+ * heuristic, both at CURRENT prices. Always labeled 估算/estimate — it is
340
+ * replaced by the projection's exact anchored value once the step settles.
341
+ * @param bucket - the current price bucket of the session model.
342
+ * @param inputTokens - the projected prompt size for the next request.
343
+ * @param cachedRatio - the cache-read ratio from recent completed steps.
344
+ * @param outputTokens - the streamed output tokens so far.
345
+ * @returns the estimated CNY cost.
346
+ */
347
+ function estimateCost(bucket, inputTokens, cachedRatio, outputTokens) {
348
+ return inputTokens / 1e6 * (bucket.cacheReadPerMillion * cachedRatio + bucket.inputPerMillion * (1 - cachedRatio)) + outputTokens / 1e6 * bucket.outputPerMillion;
349
+ }
350
+ //#endregion
351
+ //#region src/client/format.ts
352
+ /**
353
+ * Shared display formatting for amounts and timestamps.
354
+ *
355
+ * @module @gamegeek-saikel/dsh-cost-meter/client/format
356
+ */
357
+ /**
358
+ * Common currency symbols; unknown currencies render as a bare code.
359
+ * @param currency - ISO 4217 code.
360
+ * @returns the display symbol.
361
+ */
362
+ function currencySymbol(currency) {
363
+ switch (currency) {
364
+ case "CNY": return "¥";
365
+ case "USD": return "$";
366
+ case "EUR": return "€";
367
+ default: return `${currency} `;
368
+ }
369
+ }
370
+ /** Detect the display currency from the active locale dictionary. */
371
+ function displayCurrency(t) {
372
+ return t("view.cost") === "Cost" ? "USD" : "CNY";
373
+ }
374
+ /**
375
+ * Compact money: two decimals above 1, four down to 0.01, six below.
376
+ * Costs are CNY 元 with 4-decimal display precision; zero renders as 0.00.
377
+ * @param value - the amount.
378
+ * @returns the fixed-width display string.
379
+ */
380
+ function formatMoney(value) {
381
+ if (value === 0) return "0.00";
382
+ if (value >= 1) return value.toFixed(2);
383
+ if (value >= .01) return value.toFixed(4);
384
+ return value.toFixed(6);
385
+ }
386
+ /** Time formatting for snapshot stamps and step rows. */
387
+ function formatTime(epochMs) {
388
+ return new Date(epochMs).toLocaleTimeString([], {
389
+ hour: "2-digit",
390
+ minute: "2-digit",
391
+ second: "2-digit"
392
+ });
393
+ }
394
+ /**
395
+ * Compact price multiplier, e.g. `2.0×` or `0.5×`.
396
+ * @param multiplier - the ratio to display.
397
+ * @returns the fixed one-decimal multiplier string.
398
+ */
399
+ function formatMultiplier(multiplier) {
400
+ return `${multiplier.toFixed(1)}×`;
401
+ }
402
+ //#endregion
403
+ //#region \0dsh-css:D:\Projects\dsh-plugins\dsh-cost-meter\src\client\SessionCostLine.module.css.mjs
404
+ const css$4 = ".ScWO-a_root{text-align:center;max-width:var(--dsh-chat-content-width);box-sizing:border-box;width:100%;padding:0 calc(var(--dsh-composer-side-clearance) + 16px) 4px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;margin:0 auto;font-size:12px;line-height:20px;display:block;overflow:hidden}";
405
+ const tagId$4 = "@gamegeek-saikel/dsh-cost-meter/SessionCostLine.module.css";
406
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
407
+ const tag = document.createElement("style");
408
+ tag.dataset.plugin = "@gamegeek-saikel/dsh-cost-meter";
409
+ tag.dataset.pluginCss = tagId$4;
410
+ tag.textContent = css$4;
411
+ document.head.appendChild(tag);
412
+ }
413
+ var SessionCostLine_module_css_default = { "root": "ScWO-a_root" };
414
+ //#endregion
415
+ //#region src/client/SessionCostLine.tsx
416
+ /**
417
+ * Composer-dock readout: the current session's anchored spend (the
418
+ * `sessionCost` projection — price-snapshot-anchored, never recomputed at
419
+ * current prices) plus the DeepSeek account balance (host
420
+ * `/cost-meter` route), refreshed every minute. The hover detail
421
+ * breaks the spend into the three billed categories (uncached input incl.
422
+ * cache writes, cache reads, output) and names the anchored price snapshot.
423
+ *
424
+ * The component is deliberately defensive: every derived string is computed
425
+ * inside a guarded block so a single bad field can never take down the whole
426
+ * dock entry (the slot system retires crashed entries).
427
+ */
428
+ /** Host route serving the balance snapshot and the pricebook view. */
429
+ const ENDPOINT = "/cost-meter";
430
+ /** Balance refresh interval in milliseconds. */
431
+ const REFRESH_MS = 6e4;
432
+ /** Compose the tooltip's balance detail from an ok snapshot. */
433
+ function balanceTooltip(balance, t) {
434
+ if (balance.balance === null) return t("balance.failed");
435
+ const symbol = currencySymbol(balance.balance.currency);
436
+ const detail = t("balance.detail", {
437
+ amount: `${symbol}${formatMoney(balance.balance.total)}`,
438
+ granted: `${symbol}${formatMoney(balance.balance.granted)}`,
439
+ toppedUp: `${symbol}${formatMoney(balance.balance.toppedUp)}`
440
+ });
441
+ return balance.isAvailable ? detail : `${detail} · ${t("balance.suspended")}`;
442
+ }
443
+ /** The tooltip's cost detail rows from the anchored projection. Never throws. */
444
+ function costDetail(cost, model, t, currency = "CNY") {
445
+ const rows = [];
446
+ try {
447
+ if (cost === void 0) return rows;
448
+ const symbol = currencySymbol(currency);
449
+ const totals = cost.totals;
450
+ if (totals !== void 0 && totals.pricedSteps > 0) {
451
+ rows.push(t("cost.label", { amount: `${symbol}${formatMoney(totals.cost)}` }));
452
+ rows.push(t("view.uncached") + ` ${symbol}${formatMoney(totals.uncachedCost)}`);
453
+ rows.push(t("view.cacheRead") + ` ${symbol}${formatMoney(totals.cacheReadCost)}`);
454
+ rows.push(t("view.output") + ` ${symbol}${formatMoney(totals.outputCost)}`);
455
+ }
456
+ if (model !== void 0) rows.push(t("step.model", { model }));
457
+ const steps = cost.steps;
458
+ if (steps !== void 0 && steps.length > 0) {
459
+ const last = steps[steps.length - 1];
460
+ if (last !== void 0 && last.snapshotVersion !== null) rows.push(t("pill.detail.snapshot", { time: formatTime(last.snapshotEffectiveAt ?? last.time) }));
461
+ }
462
+ if (totals !== void 0 && totals.unpricedSteps > 0) rows.push(t("view.unpriced", { count: String(totals.unpricedSteps) }));
463
+ } catch {}
464
+ return rows;
465
+ }
466
+ const SessionCostLine = (0, react.memo)(function SessionCostLine({ useProjection, sessionId, t }) {
467
+ const currency = displayCurrency(t);
468
+ const cost = useProjection(currency === "USD" ? "sessionCostUsd" : "sessionCost");
469
+ const [response, setResponse] = (0, react.useState)(null);
470
+ const [failed, setFailed] = (0, react.useState)(false);
471
+ (0, react.useEffect)(() => {
472
+ let alive = true;
473
+ let timer;
474
+ const endpoint = `${ENDPOINT}?session=${encodeURIComponent(sessionId)}&currency=${currency}`;
475
+ const load = () => {
476
+ fetch(endpoint, { cache: "no-store" }).then((res) => {
477
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
478
+ return res.json();
479
+ }).then((data) => {
480
+ if (!alive) return;
481
+ setResponse(data);
482
+ setFailed(false);
483
+ }).catch(() => {
484
+ if (alive) setFailed(true);
485
+ });
486
+ };
487
+ load();
488
+ timer = window.setInterval(load, REFRESH_MS);
489
+ return () => {
490
+ alive = false;
491
+ if (timer !== void 0) window.clearInterval(timer);
492
+ };
493
+ }, [sessionId, currency]);
494
+ const balance = response?.balance;
495
+ const balanceEnabled = response?.pricebook?.balanceEnabled !== false;
496
+ const subagents = response?.subagents;
497
+ const combined = combineTotals(cost?.totals, subagents);
498
+ const subSpend = subagentSpend(subagents);
499
+ const mainSpend = cost?.totals?.cost ?? 0;
500
+ const parts = [];
501
+ try {
502
+ const symbol = currencySymbol(currency);
503
+ if (combined !== void 0 && combined.pricedSteps > 0) {
504
+ parts.push(t("line.cacheRead", { amount: `${symbol}${formatMoney(combined.cacheReadCost)}` }));
505
+ parts.push(t("line.uncached", { amount: `${symbol}${formatMoney(combined.uncachedCost)}` }));
506
+ parts.push(t("line.output", { amount: `${symbol}${formatMoney(combined.outputCost)}` }));
507
+ }
508
+ parts.push(t("cost.label", { amount: `${symbol}${formatMoney(mainSpend)}` }));
509
+ if (subSpend > 0) parts.push(t("line.subagent", { amount: `${symbol}${formatMoney(subSpend)}` }));
510
+ if (balanceEnabled && balance?.ok === true && balance.balance !== null) parts.push(t("balance.label", { amount: `${currencySymbol(balance.balance.currency)}${formatMoney(balance.balance.total)}` }));
511
+ else if (balanceEnabled && failed) parts.push(t("balance.failed"));
512
+ } catch {}
513
+ if (parts.length === 0) return null;
514
+ const model = cost?.model?.model;
515
+ const tooltipParts = (0, react.useMemo)(() => {
516
+ const rows = [];
517
+ try {
518
+ rows.push(...costDetail(cost, model, t, currency));
519
+ if (subSpend > 0) rows.push(t("line.subagent", { amount: `${currencySymbol(currency)}${formatMoney(subSpend)}` }));
520
+ const pricebook = response?.pricebook;
521
+ if (pricebook !== void 0 && pricebook.current !== null) {
522
+ const source = pricebook.current.source;
523
+ if (source === "official" || source === "fallback" || source === "manual" || source === "openrouter") rows.push(t("pill.detail.source", { source: t(`source.${source}`) }));
524
+ }
525
+ if (balanceEnabled && balance?.ok === true) rows.push(balanceTooltip(balance, t));
526
+ if (response !== null) rows.push(t("refreshedAt", { time: formatTime(balance?.ok === true ? balance.fetchedAt : Date.now()) }));
527
+ } catch {}
528
+ return rows;
529
+ }, [
530
+ cost,
531
+ model,
532
+ t,
533
+ balance,
534
+ balanceEnabled,
535
+ response,
536
+ subSpend,
537
+ currency
538
+ ]);
539
+ const line = parts.join(" · ");
540
+ const tooltip = tooltipParts.filter(Boolean).join(" · ");
541
+ if (tooltip.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
542
+ className: SessionCostLine_module_css_default.root,
543
+ "data-testid": "session-cost-line",
544
+ children: line
545
+ });
546
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
547
+ label: tooltip,
548
+ side: "top",
549
+ delayMs: 500,
550
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
551
+ className: SessionCostLine_module_css_default.root,
552
+ "data-testid": "session-cost-line",
553
+ children: line
554
+ })
555
+ });
556
+ });
557
+ //#endregion
558
+ //#region \0dsh-css:D:\Projects\dsh-plugins\dsh-cost-meter\src\client\CostView.module.css.mjs
559
+ const css$3 = ".l7-mcW_root{max-width:var(--dsh-chat-content-width);box-sizing:border-box;width:100%;color:var(--dsw-alias-label-primary);flex-direction:column;gap:16px;margin:0 auto;padding:16px 24px;font-size:13px;line-height:20px;display:flex}.l7-mcW_summary{flex-direction:column;gap:8px;display:flex}.l7-mcW_totalRow{justify-content:space-between;align-items:baseline;display:flex}.l7-mcW_totalLabel{font-size:15px;font-weight:600}.l7-mcW_totalValue{font-variant-numeric:tabular-nums;font-size:18px;font-weight:700}.l7-mcW_bandBadge{opacity:.85;white-space:nowrap;font-size:11px;font-weight:500}.l7-mcW_bandBadge[data-band=peak]{color:var(--dsw-alias-state-error-primary)}.l7-mcW_bandBadge[data-band=offPeak]{color:var(--dsw-alias-state-success-primary)}.l7-mcW_bandBadge:before{content:\"·\";color:currentColor;opacity:.6;margin:0 4px}.l7-mcW_categories{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex-wrap:wrap;gap:4px 16px;font-size:12px;display:flex}.l7-mcW_unpricedNote{color:var(--dsw-alias-label-secondary);opacity:.75;font-size:12px}.l7-mcW_steps{flex-direction:column;gap:8px;display:flex}.l7-mcW_stepsTitle{margin:0;font-size:13px;font-weight:600}.l7-mcW_stepList{flex-direction:column;gap:6px;margin:0;padding:0;list-style:none;display:flex}.l7-mcW_step{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:8px;flex-direction:column;gap:2px;padding:8px 10px;display:flex}.l7-mcW_step[data-band=peak]{border-color:var(--dsw-alias-state-error-secondary)}.l7-mcW_step[data-band=offPeak]{border-color:var(--dsw-alias-state-success-secondary)}.l7-mcW_step[data-band=peak] .l7-mcW_stepCost{color:var(--dsw-alias-state-error-primary)}.l7-mcW_step[data-band=offPeak] .l7-mcW_stepCost{color:var(--dsw-alias-state-success-primary)}.l7-mcW_stepHead{flex-wrap:wrap;align-items:baseline;gap:8px;display:flex}.l7-mcW_stepTurn{font-weight:600}.l7-mcW_stepModel{color:var(--dsw-alias-label-secondary);font-size:12px}.l7-mcW_stepTime{color:var(--dsw-alias-label-secondary);opacity:.75;font-size:12px}.l7-mcW_stepCost{font-variant-numeric:tabular-nums;align-items:baseline;gap:4px;margin-left:auto;font-weight:600;display:inline-flex}.l7-mcW_stepBody{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex-wrap:wrap;gap:12px;font-size:12px;display:flex}.l7-mcW_stepMeta{color:var(--dsw-alias-label-secondary);opacity:.75}";
560
+ const tagId$3 = "@gamegeek-saikel/dsh-cost-meter/CostView.module.css";
561
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
562
+ const tag = document.createElement("style");
563
+ tag.dataset.plugin = "@gamegeek-saikel/dsh-cost-meter";
564
+ tag.dataset.pluginCss = tagId$3;
565
+ tag.textContent = css$3;
566
+ document.head.appendChild(tag);
567
+ }
568
+ var CostView_module_css_default = {
569
+ "bandBadge": "l7-mcW_bandBadge",
570
+ "categories": "l7-mcW_categories",
571
+ "root": "l7-mcW_root",
572
+ "step": "l7-mcW_step",
573
+ "stepBody": "l7-mcW_stepBody",
574
+ "stepCost": "l7-mcW_stepCost",
575
+ "stepHead": "l7-mcW_stepHead",
576
+ "stepList": "l7-mcW_stepList",
577
+ "stepMeta": "l7-mcW_stepMeta",
578
+ "stepModel": "l7-mcW_stepModel",
579
+ "stepTime": "l7-mcW_stepTime",
580
+ "stepTurn": "l7-mcW_stepTurn",
581
+ "steps": "l7-mcW_steps",
582
+ "stepsTitle": "l7-mcW_stepsTitle",
583
+ "summary": "l7-mcW_summary",
584
+ "totalLabel": "l7-mcW_totalLabel",
585
+ "totalRow": "l7-mcW_totalRow",
586
+ "totalValue": "l7-mcW_totalValue",
587
+ "unpricedNote": "l7-mcW_unpricedNote"
588
+ };
589
+ //#endregion
590
+ //#region src/client/CostView.tsx
591
+ /**
592
+ * Cost view tab: the whole conversation's anchored cost ledger — the total
593
+ * (main session + subagent sessions, host-aggregated), the three billed
594
+ * categories (uncached input incl. cache writes, cache reads, output), a
595
+ * per-subagent breakdown, and the per-reply list (turn, time, model, token
596
+ * split, step cost, anchored snapshot version and effective time). Reads the
597
+ * `sessionCost` projection plus the host-aggregated subagent totals; nothing
598
+ * is recomputed at current prices.
599
+ */
600
+ /** Map an unpriced reason to its localized copy. */
601
+ function reasonText(step, t) {
602
+ return t(`reason.${step.unpricedReason ?? "NO_PRICE"}`);
603
+ }
604
+ const CostView = (0, react.memo)(function CostView({ useProjection, sessionId, t }) {
605
+ const currency = displayCurrency(t);
606
+ const cost = useProjection(currency === "USD" ? "sessionCostUsd" : "sessionCost");
607
+ const [response, setResponse] = (0, react.useState)(null);
608
+ const symbol = currencySymbol(currency);
609
+ (0, react.useEffect)(() => {
610
+ let alive = true;
611
+ fetch(`/cost-meter?session=${encodeURIComponent(sessionId)}&currency=${currency}`, { cache: "no-store" }).then((res) => res.ok ? res.json() : null).then((data) => {
612
+ if (!alive || data === null) return;
613
+ setResponse(data);
614
+ }).catch(() => {});
615
+ return () => {
616
+ alive = false;
617
+ };
618
+ }, [sessionId, currency]);
619
+ if (cost === void 0 || cost.steps.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
620
+ className: CostView_module_css_default.root,
621
+ "data-testid": "cost-view-empty",
622
+ children: t("view.empty")
623
+ });
624
+ const rows = cost.steps;
625
+ const totals = combineTotals(cost.totals, response?.subagents) ?? cost.totals;
626
+ const subSpend = subagentSpend(response?.subagents);
627
+ const money = (value) => value === null ? "—" : `${symbol}${formatMoney(value)}`;
628
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
629
+ className: CostView_module_css_default.root,
630
+ "data-testid": "cost-view",
631
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
632
+ className: CostView_module_css_default.summary,
633
+ children: [
634
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
635
+ className: CostView_module_css_default.totalRow,
636
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
637
+ className: CostView_module_css_default.totalLabel,
638
+ children: t("view.total")
639
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
640
+ className: CostView_module_css_default.totalValue,
641
+ children: money(totals.cost)
642
+ })]
643
+ }),
644
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
645
+ className: CostView_module_css_default.categories,
646
+ children: [
647
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
648
+ t("view.uncached"),
649
+ ": ",
650
+ money(totals.uncachedCost)
651
+ ] }),
652
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
653
+ t("view.cacheRead"),
654
+ ": ",
655
+ money(totals.cacheReadCost)
656
+ ] }),
657
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
658
+ t("view.output"),
659
+ ": ",
660
+ money(totals.outputCost)
661
+ ] })
662
+ ]
663
+ }),
664
+ totals.unpricedSteps > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
665
+ className: CostView_module_css_default.unpricedNote,
666
+ children: t("view.unpriced", { count: String(totals.unpricedSteps) })
667
+ }),
668
+ response !== null && response.subagents.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
669
+ className: CostView_module_css_default.subagents,
670
+ "data-testid": "cost-view-subagents",
671
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
672
+ className: CostView_module_css_default.unpricedNote,
673
+ children: t("line.subagent", { amount: `${symbol}${formatMoney(subSpend)}` })
674
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
675
+ className: CostView_module_css_default.stepList,
676
+ children: response.subagents.map((sub) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
677
+ className: CostView_module_css_default.step,
678
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
679
+ className: CostView_module_css_default.stepHead,
680
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
681
+ className: CostView_module_css_default.stepTurn,
682
+ children: sub.sessionId.slice(0, 8)
683
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
684
+ className: CostView_module_css_default.stepCost,
685
+ children: money(sub.totals.cost)
686
+ })]
687
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
688
+ className: CostView_module_css_default.stepBody,
689
+ children: [
690
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
691
+ t("view.uncached"),
692
+ ": ",
693
+ money(sub.totals.uncachedCost)
694
+ ] }),
695
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
696
+ t("view.cacheRead"),
697
+ ": ",
698
+ money(sub.totals.cacheReadCost)
699
+ ] }),
700
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
701
+ t("view.output"),
702
+ ": ",
703
+ money(sub.totals.outputCost)
704
+ ] })
705
+ ]
706
+ })]
707
+ }, sub.sessionId))
708
+ })]
709
+ })
710
+ ]
711
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
712
+ className: CostView_module_css_default.steps,
713
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
714
+ className: CostView_module_css_default.stepsTitle,
715
+ children: t("view.steps")
716
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
717
+ className: CostView_module_css_default.stepList,
718
+ children: rows.map((step) => {
719
+ const stepBand = bandForTime(Date.now());
720
+ const stepRatio = peakOffPeakMultiplier(response?.pricebook?.current ?? null, step.provider, step.model);
721
+ const stepBandLabel = stepBand === "peak" ? stepRatio === null ? t("band.peak") : t("price.peakRatio", { multiplier: formatMultiplier(stepRatio) }) : stepBand === "offPeak" ? stepRatio === null ? t("band.offPeak") : t("price.offPeakRatio", { multiplier: formatMultiplier(1 / stepRatio) }) : null;
722
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
723
+ className: CostView_module_css_default.step,
724
+ "data-testid": "cost-step",
725
+ "data-band": stepBand === "single" ? void 0 : stepBand,
726
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
727
+ className: CostView_module_css_default.stepHead,
728
+ children: [
729
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
730
+ className: CostView_module_css_default.stepTurn,
731
+ children: t("step.turn", { turn: String(step.turn) })
732
+ }),
733
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
734
+ className: CostView_module_css_default.stepModel,
735
+ children: t("step.model", { model: step.model })
736
+ }),
737
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
738
+ className: CostView_module_css_default.stepTime,
739
+ children: formatTime(step.time)
740
+ }),
741
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
742
+ className: CostView_module_css_default.stepCost,
743
+ children: [money(step.cost), stepBandLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
744
+ className: CostView_module_css_default.bandBadge,
745
+ "data-band": stepBand,
746
+ "data-testid": "cost-step-band",
747
+ children: stepBandLabel
748
+ })]
749
+ })
750
+ ]
751
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
752
+ className: CostView_module_css_default.stepBody,
753
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
754
+ className: CostView_module_css_default.stepTokens,
755
+ children: t("step.tokens", {
756
+ uncached: String(step.uncachedInputTokens),
757
+ read: String(step.cacheReadTokens),
758
+ write: String(step.cacheWriteTokens),
759
+ output: String(step.outputTokens)
760
+ })
761
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
762
+ className: CostView_module_css_default.stepMeta,
763
+ children: step.priced && step.snapshotVersion !== null ? t("step.snapshot", { time: formatTime(step.snapshotEffectiveAt ?? step.time) }) : t("step.unpriced", { reason: reasonText(step, t) })
764
+ })]
765
+ })]
766
+ }, `${step.turn}:${step.step}`);
767
+ })
768
+ })]
769
+ })]
770
+ });
771
+ });
772
+ //#endregion
773
+ //#region \0dsh-css:D:\Projects\dsh-plugins\dsh-cost-meter\src\client\AssistantCostChip.module.css.mjs
774
+ const css$2 = ".Sy2Hbq_root{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-variant-numeric:tabular-nums;order:10;align-items:baseline;gap:4px;margin-left:10px;font-size:14px;line-height:24px;display:inline-flex}.Sy2Hbq_root:before{content:\"·\";color:var(--dsw-alias-separator-primary);margin-right:10px}.Sy2Hbq_bandBadge{opacity:.85;white-space:nowrap;font-size:12px;line-height:18px}.Sy2Hbq_bandBadge[data-band=peak]{color:var(--dsw-alias-state-error-primary)}.Sy2Hbq_bandBadge[data-band=offPeak]{color:var(--dsw-alias-state-success-primary)}.Sy2Hbq_bandBadge:before{content:\"·\";color:currentColor;opacity:.6;margin:0 4px}.Sy2Hbq_unpriced{color:var(--dsw-alias-label-tertiary);white-space:nowrap;opacity:.6;cursor:help;order:10;margin-left:10px;font-size:14px;line-height:24px}.Sy2Hbq_unpriced:before{content:\"·\";color:var(--dsw-alias-separator-primary);margin-right:10px}@media (hover:hover){[data-time-hover-root] [data-cost-chip]{opacity:0;transition:opacity 80ms}[data-time-hover-root]:hover [data-cost-chip],[data-time-hover-root]:focus-within [data-cost-chip]{opacity:1}[data-time-hover-root] [data-cost-chip].Sy2Hbq_unpriced{opacity:0}}";
775
+ const tagId$2 = "@gamegeek-saikel/dsh-cost-meter/AssistantCostChip.module.css";
776
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
777
+ const tag = document.createElement("style");
778
+ tag.dataset.plugin = "@gamegeek-saikel/dsh-cost-meter";
779
+ tag.dataset.pluginCss = tagId$2;
780
+ tag.textContent = css$2;
781
+ document.head.appendChild(tag);
782
+ }
783
+ var AssistantCostChip_module_css_default = {
784
+ "bandBadge": "Sy2Hbq_bandBadge",
785
+ "root": "Sy2Hbq_root",
786
+ "unpriced": "Sy2Hbq_unpriced"
787
+ };
788
+ //#endregion
789
+ //#region src/client/AssistantCostChip.tsx
790
+ /**
791
+ * Per-reply cost chip: the anchored cost of ONE finalized reply. Rendered
792
+ * through the assistant-actions slot but visually joined to the reply's
793
+ * timing row (01:51 · 用时 · 首 token · tok/s): a flex `order` places it
794
+ * after the clock text, and the type matches the clock's (label-tertiary,
795
+ * 14px) with a dot separator, so it reads as part of the same metadata run.
796
+ * The `messageId` owner prop maps to the assistant node's (turn, step),
797
+ * which addresses the projection's step ledger — the cost is the anchored
798
+ * snapshot value, never a current-price recompute. Unpriced replies render
799
+ * `—` (the Cost tab explains why).
800
+ */
801
+ /**
802
+ * Locate a finalized assistant node by its message id and return its
803
+ * (turn, step) coordinates, or null when the node is out of window.
804
+ * @param snapshot - the conversation snapshot.
805
+ * @param messageId - the addressed message identity.
806
+ * @returns the step coordinates, or null.
807
+ */
808
+ function stepOfMessage(snapshot, messageId) {
809
+ const nodes = snapshot.chat.legacy.nodes;
810
+ for (let i = nodes.length - 1; i >= 0; i -= 1) {
811
+ const node = nodes[i];
812
+ if (node?.kind === "assistant" && node.messageId === messageId && node.turn !== void 0) return {
813
+ turn: node.turn,
814
+ step: node.step
815
+ };
816
+ }
817
+ return null;
818
+ }
819
+ /** The projection step for one (turn, step) coordinate, if the ledger has it. */
820
+ function stepOf(steps, turn, step) {
821
+ return steps?.find((entry) => entry.turn === turn && entry.step === step);
822
+ }
823
+ const AssistantCostChip = (0, react.memo)(function AssistantCostChip({ messageId, useSession, useProjection, t }) {
824
+ const currency = displayCurrency(t);
825
+ const projectionKey = currency === "USD" ? "sessionCostUsd" : "sessionCost";
826
+ const step = useSession((snapshot) => stepOfMessage(snapshot, messageId));
827
+ const cost = useProjection(projectionKey);
828
+ const ledger = step === null ? void 0 : stepOf(cost?.steps, step.turn, step.step);
829
+ const [response, setResponse] = (0, react.useState)(null);
830
+ (0, react.useEffect)(() => {
831
+ let alive = true;
832
+ fetch(`/cost-meter?currency=${currency}`, { cache: "no-store" }).then((res) => res.ok ? res.json() : null).then((data) => {
833
+ if (!alive || data === null) return;
834
+ setResponse(data);
835
+ }).catch(() => {});
836
+ return () => {
837
+ alive = false;
838
+ };
839
+ }, [currency]);
840
+ if (ledger === void 0 || ledger.cost === null) {
841
+ if (ledger === void 0) return null;
842
+ const reason = ledger.unpricedReason === "NO_MODEL" ? t("reason.NO_MODEL") : t("reason.NO_PRICE");
843
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
844
+ className: AssistantCostChip_module_css_default.unpriced,
845
+ "data-cost-chip": true,
846
+ title: reason,
847
+ "data-testid": "cost-chip-unpriced",
848
+ children: t("chip.unpriced")
849
+ });
850
+ }
851
+ const amount = `${currencySymbol(currency)}${formatMoney(ledger.cost)}`;
852
+ const band = bandForTime(Date.now());
853
+ const ratio = peakOffPeakMultiplier(response?.pricebook?.current ?? null, ledger.provider, ledger.model);
854
+ const bandLabel = band === "peak" ? ratio === null ? t("band.peak") : t("price.peakRatio", { multiplier: formatMultiplier(ratio) }) : band === "offPeak" ? ratio === null ? t("band.offPeak") : t("price.offPeakRatio", { multiplier: formatMultiplier(1 / ratio) }) : null;
855
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
856
+ className: AssistantCostChip_module_css_default.root,
857
+ "data-cost-chip": true,
858
+ title: t("chip.title", { amount }),
859
+ "data-testid": "cost-chip",
860
+ children: [amount, bandLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
861
+ className: AssistantCostChip_module_css_default.bandBadge,
862
+ "data-band": band,
863
+ "data-testid": "cost-chip-band",
864
+ children: bandLabel
865
+ })]
866
+ });
867
+ });
868
+ //#endregion
869
+ //#region \0dsh-css:D:\Projects\dsh-plugins\dsh-cost-meter\src\client\SessionCostPill.module.css.mjs
870
+ const css$1 = "._1P-OIW_wrap{display:inline-flex;position:relative}._1P-OIW_pill{border:1px solid var(--dsw-alias-border-l2);height:32px;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);font-variant-numeric:tabular-nums;white-space:nowrap;cursor:pointer;user-select:none;background:0 0;border-radius:18px;justify-content:center;align-items:center;gap:4px;padding:6px 12px;font-size:13px;font-weight:400;line-height:20px;transition:background-color .15s,color .15s,border-color .15s;display:inline-flex}._1P-OIW_pill:hover{background:var(--dsw-alias-interactive-bg-hover)}._1P-OIW_pill:active{transform:translateY(.5px)}._1P-OIW_pill[data-running=true]{color:var(--dsw-alias-brand-primary)}._1P-OIW_pill[data-running=true]:hover{background:var(--dsw-alias-interactive-bg-hover)}._1P-OIW_pill[data-band=peak]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-secondary)}._1P-OIW_pill[data-band=offPeak]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-secondary)}._1P-OIW_bandBadge{opacity:.85;white-space:nowrap;font-size:11px;line-height:16px}._1P-OIW_bandBadge:before{content:\"·\";color:currentColor;opacity:.6;margin:0 4px}._1P-OIW_estimateNote{opacity:.75;font-size:11px}._1P-OIW_detail{z-index:100;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu);min-width:250px;max-width:340px;box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;border-radius:12px;flex-direction:column;gap:5px;padding:12px 14px;font-size:12px;line-height:18px;display:flex;position:absolute;top:calc(100% + 8px);right:0}._1P-OIW_detailRow{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}._1P-OIW_detailRowSecondary{color:var(--dsw-alias-label-secondary)}._1P-OIW_detailRowMuted{color:var(--dsw-alias-label-secondary);opacity:.75}._1P-OIW_detailTotal{font-size:13px;font-weight:600}";
871
+ const tagId$1 = "@gamegeek-saikel/dsh-cost-meter/SessionCostPill.module.css";
872
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
873
+ const tag = document.createElement("style");
874
+ tag.dataset.plugin = "@gamegeek-saikel/dsh-cost-meter";
875
+ tag.dataset.pluginCss = tagId$1;
876
+ tag.textContent = css$1;
877
+ document.head.appendChild(tag);
878
+ }
879
+ var SessionCostPill_module_css_default = {
880
+ "bandBadge": "_1P-OIW_bandBadge",
881
+ "detail": "_1P-OIW_detail",
882
+ "detailRow": "_1P-OIW_detailRow",
883
+ "detailRowMuted": "_1P-OIW_detailRowMuted",
884
+ "detailRowSecondary": "_1P-OIW_detailRowSecondary",
885
+ "detailTotal": "_1P-OIW_detailTotal",
886
+ "estimateNote": "_1P-OIW_estimateNote",
887
+ "pill": "_1P-OIW_pill",
888
+ "wrap": "_1P-OIW_wrap"
889
+ };
890
+ //#endregion
891
+ //#region src/client/SessionCostPill.tsx
892
+ /**
893
+ * Session header pill (right-aligned utility): the conversation's anchored
894
+ * total cost, or — while the assistant is replying — a live estimate of the
895
+ * TOTAL once the running reply settles (`预计 ¥x.xx(进行中)`): the already
896
+ * anchored ledger total plus the running reply's estimated input/output.
897
+ * Clicking the pill toggles a detail panel: totals by category, model, and
898
+ * the anchored price snapshot; the panel dismisses on outside clicks or
899
+ * Escape.
900
+ *
901
+ * The estimate is explicitly labeled 估算/estimate: input is priced at the
902
+ * projected context size weighted by the newest completed step's cache
903
+ * ratio (conservative all-uncached without history), output at the
904
+ * character heuristic — both at CURRENT snapshot prices.
905
+ */
906
+ /** Read the conversation's running flag (the pill's estimate gate). */
907
+ function runningOf(snapshot) {
908
+ return snapshot.running;
909
+ }
910
+ /** Close the popover on outside clicks or Escape (product detail-panel posture). */
911
+ function useDismissOutside(open, onDismiss, anchor) {
912
+ (0, react.useEffect)(() => {
913
+ if (!open) return;
914
+ const onPointerDown = (event) => {
915
+ const target = event.target;
916
+ if (anchor.current !== null && target !== null && !anchor.current.contains(target)) onDismiss();
917
+ };
918
+ const onKeyDown = (event) => {
919
+ if (event.key === "Escape") onDismiss();
920
+ };
921
+ document.addEventListener("mousedown", onPointerDown);
922
+ document.addEventListener("touchstart", onPointerDown);
923
+ document.addEventListener("keydown", onKeyDown);
924
+ return () => {
925
+ document.removeEventListener("mousedown", onPointerDown);
926
+ document.removeEventListener("touchstart", onPointerDown);
927
+ document.removeEventListener("keydown", onKeyDown);
928
+ };
929
+ }, [
930
+ open,
931
+ onDismiss,
932
+ anchor
933
+ ]);
934
+ }
935
+ const SessionCostPill = (0, react.memo)(function SessionCostPill({ useSession, useProjection, sessionId, t }) {
936
+ const currency = displayCurrency(t);
937
+ const cost = useProjection(currency === "USD" ? "sessionCostUsd" : "sessionCost");
938
+ const pressure = useProjection("contextPressure");
939
+ const running = useSession(runningOf);
940
+ const partial = useSession((snapshot) => snapshot.partial);
941
+ const [open, setOpen] = (0, react.useState)(false);
942
+ const [response, setResponse] = (0, react.useState)(null);
943
+ const wrapRef = (0, react.useRef)(null);
944
+ useDismissOutside(open, () => setOpen(false), wrapRef);
945
+ (0, react.useEffect)(() => {
946
+ let alive = true;
947
+ let timer;
948
+ const endpoint = `${ENDPOINT}?session=${encodeURIComponent(sessionId)}&currency=${currency}`;
949
+ const load = () => {
950
+ fetch(endpoint, { cache: "no-store" }).then((res) => res.ok ? res.json() : null).then((data) => {
951
+ if (!alive || data === null) return;
952
+ setResponse(data);
953
+ }).catch(() => {});
954
+ };
955
+ load();
956
+ timer = window.setInterval(load, REFRESH_MS);
957
+ return () => {
958
+ alive = false;
959
+ if (timer !== void 0) window.clearInterval(timer);
960
+ };
961
+ }, [sessionId, currency]);
962
+ const projection = cost;
963
+ const model = projection?.model ?? null;
964
+ const snapshot = response?.pricebook.current ?? null;
965
+ const subagents = response?.subagents;
966
+ const combined = combineTotals(projection?.totals, subagents);
967
+ const subSpend = subagentSpend(subagents);
968
+ const symbol = currencySymbol(currency);
969
+ const estimate = (0, react.useMemo)(() => {
970
+ if (!running || projection === void 0) return null;
971
+ const bucket = bucketAt(snapshot ?? void 0, model?.provider, model?.model ?? "unknown", Date.now());
972
+ if (bucket === void 0) return null;
973
+ const stepEstimate = estimateCost(bucket, pressure?.projectedTokens ?? pressure?.pressureTokens ?? 0, cacheReadRatioOf(projection.steps), blocksOutputTokens(partial?.blocks));
974
+ return (combined?.cost ?? 0) + stepEstimate;
975
+ }, [
976
+ running,
977
+ projection,
978
+ combined,
979
+ snapshot,
980
+ model,
981
+ pressure,
982
+ partial,
983
+ currency
984
+ ]);
985
+ if (projection === void 0 && estimate === null) return null;
986
+ const total = combined?.cost ?? 0;
987
+ const label = estimate !== null ? t("pill.estimate", { amount: `${symbol}${formatMoney(estimate)}` }) : t("pill.label", { amount: `${symbol}${formatMoney(total)}` });
988
+ const last = projection?.steps.length !== void 0 && projection.steps.length > 0 ? projection.steps[projection.steps.length - 1] : void 0;
989
+ const sourceKey = snapshot?.source ?? null;
990
+ const source = sourceKey === null ? t("source.none") : t(`source.${sourceKey}`);
991
+ const band = bandForTime(Date.now());
992
+ const peakRatio = peakOffPeakMultiplier(snapshot ?? void 0, model?.provider, model?.model ?? "unknown");
993
+ const bandLabel = band === "peak" ? peakRatio === null ? t("band.peak") : t("price.peakRatio", { multiplier: formatMultiplier(peakRatio) }) : band === "offPeak" ? peakRatio === null ? t("band.offPeak") : t("price.offPeakRatio", { multiplier: formatMultiplier(1 / peakRatio) }) : null;
994
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
995
+ className: SessionCostPill_module_css_default.wrap,
996
+ ref: wrapRef,
997
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
998
+ type: "button",
999
+ className: SessionCostPill_module_css_default.pill,
1000
+ "data-testid": "cost-pill",
1001
+ "data-running": estimate !== null,
1002
+ "data-band": band === "single" ? void 0 : band,
1003
+ onClick: () => setOpen((prev) => !prev),
1004
+ "aria-expanded": open,
1005
+ children: [
1006
+ label,
1007
+ bandLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1008
+ className: SessionCostPill_module_css_default.bandBadge,
1009
+ "data-testid": "cost-pill-band",
1010
+ children: bandLabel
1011
+ }),
1012
+ estimate !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1013
+ className: SessionCostPill_module_css_default.estimateNote,
1014
+ children: ["· ", t("pill.estimateNote")]
1015
+ })
1016
+ ]
1017
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1018
+ className: SessionCostPill_module_css_default.detail,
1019
+ "data-testid": "cost-pill-detail",
1020
+ children: [
1021
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1022
+ className: SessionCostPill_module_css_default.detailRow + " " + SessionCostPill_module_css_default.detailTotal,
1023
+ children: t("pill.detail.total", { amount: `${symbol}${formatMoney(total)}` })
1024
+ }),
1025
+ bandLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1026
+ className: SessionCostPill_module_css_default.detailRow + " " + SessionCostPill_module_css_default.detailRowSecondary,
1027
+ "data-testid": "cost-pill-detail-band",
1028
+ children: bandLabel
1029
+ }),
1030
+ combined !== void 0 && combined.pricedSteps > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1031
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1032
+ className: SessionCostPill_module_css_default.detailRow + " " + SessionCostPill_module_css_default.detailRowSecondary,
1033
+ children: [
1034
+ t("view.uncached"),
1035
+ ": ",
1036
+ symbol,
1037
+ formatMoney(combined.uncachedCost)
1038
+ ]
1039
+ }),
1040
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1041
+ className: SessionCostPill_module_css_default.detailRow + " " + SessionCostPill_module_css_default.detailRowSecondary,
1042
+ children: [
1043
+ t("view.cacheRead"),
1044
+ ": ",
1045
+ symbol,
1046
+ formatMoney(combined.cacheReadCost)
1047
+ ]
1048
+ }),
1049
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1050
+ className: SessionCostPill_module_css_default.detailRow + " " + SessionCostPill_module_css_default.detailRowSecondary,
1051
+ children: [
1052
+ t("view.output"),
1053
+ ": ",
1054
+ symbol,
1055
+ formatMoney(combined.outputCost)
1056
+ ]
1057
+ })
1058
+ ] }),
1059
+ subSpend > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1060
+ className: SessionCostPill_module_css_default.detailRow + " " + SessionCostPill_module_css_default.detailRowMuted,
1061
+ children: t("line.subagent", { amount: `${symbol}${formatMoney(subSpend)}` })
1062
+ }),
1063
+ model !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1064
+ className: SessionCostPill_module_css_default.detailRow + " " + SessionCostPill_module_css_default.detailRowSecondary,
1065
+ children: t("pill.detail.model", { model: model.model })
1066
+ }),
1067
+ last !== void 0 && last.snapshotVersion !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1068
+ className: SessionCostPill_module_css_default.detailRow + " " + SessionCostPill_module_css_default.detailRowMuted,
1069
+ children: [t("pill.detail.snapshot", { time: formatTime(last.snapshotEffectiveAt ?? last.time) }), last.band !== null && ` · ${t(`band.${last.band}`)}`]
1070
+ }),
1071
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1072
+ className: SessionCostPill_module_css_default.detailRow + " " + SessionCostPill_module_css_default.detailRowMuted,
1073
+ children: t("pill.detail.source", { source })
1074
+ }),
1075
+ projection !== void 0 && projection.totals.unpricedSteps > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1076
+ className: SessionCostPill_module_css_default.detailRow + " " + SessionCostPill_module_css_default.detailRowMuted,
1077
+ children: t("view.unpriced", { count: String(projection.totals.unpricedSteps) })
1078
+ }),
1079
+ estimate !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1080
+ className: SessionCostPill_module_css_default.detailRow + " " + SessionCostPill_module_css_default.detailRowMuted,
1081
+ children: t("pill.estimateNote")
1082
+ })
1083
+ ]
1084
+ })]
1085
+ });
1086
+ });
1087
+ //#endregion
1088
+ //#region \0dsh-css:D:\Projects\dsh-plugins\dsh-cost-meter\src\client\CostPluginCard.module.css.mjs
1089
+ const css = ".XNoGEa_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.XNoGEa_card:hover{border-color:var(--dsw-alias-label-dimmed)}.XNoGEa_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.XNoGEa_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.XNoGEa_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.XNoGEa_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.XNoGEa_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.XNoGEa_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.XNoGEa_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.XNoGEa_chevronOpen{transform:rotate(180deg)}.XNoGEa_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);text-overflow:ellipsis;border-radius:999px;flex:none;max-width:220px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;overflow:hidden}.XNoGEa_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.XNoGEa_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.XNoGEa_field+.XNoGEa_field{border-top:1px solid var(--dsw-alias-border-l2)}.XNoGEa_head{align-items:center;gap:8px;display:flex}.XNoGEa_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.XNoGEa_input,.XNoGEa_select,.XNoGEa_textarea{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;font-size:13px;line-height:1.5}.XNoGEa_input{width:120px;padding:5px 8px}.XNoGEa_select{padding:5px 8px}.XNoGEa_textarea{width:100%;min-height:72px;font-family:var(--dsw-font-mono,monospace);resize:vertical;padding:6px 8px;font-size:12px}.XNoGEa_inline{align-items:center;gap:8px;display:flex}.XNoGEa_checkbox{width:16px;height:16px;accent-color:var(--dsw-alias-brand-primary)}.XNoGEa_hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}.XNoGEa_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.XNoGEa_spacer{flex:1}.XNoGEa_status{color:var(--dsw-alias-state-success-primary);font-size:12px;line-height:1.5}";
1090
+ const tagId = "@gamegeek-saikel/dsh-cost-meter/CostPluginCard.module.css";
1091
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1092
+ const tag = document.createElement("style");
1093
+ tag.dataset.plugin = "@gamegeek-saikel/dsh-cost-meter";
1094
+ tag.dataset.pluginCss = tagId;
1095
+ tag.textContent = css;
1096
+ document.head.appendChild(tag);
1097
+ }
1098
+ var CostPluginCard_module_css_default = {
1099
+ "body": "XNoGEa_body",
1100
+ "card": "XNoGEa_card",
1101
+ "cardOpen": "XNoGEa_cardOpen",
1102
+ "checkbox": "XNoGEa_checkbox",
1103
+ "chevron": "XNoGEa_chevron",
1104
+ "chevronOpen": "XNoGEa_chevronOpen",
1105
+ "description": "XNoGEa_description",
1106
+ "field": "XNoGEa_field",
1107
+ "footer": "XNoGEa_footer",
1108
+ "head": "XNoGEa_head",
1109
+ "headText": "XNoGEa_headText",
1110
+ "header": "XNoGEa_header",
1111
+ "hint": "XNoGEa_hint",
1112
+ "inline": "XNoGEa_inline",
1113
+ "input": "XNoGEa_input",
1114
+ "label": "XNoGEa_label",
1115
+ "name": "XNoGEa_name",
1116
+ "pending": "XNoGEa_pending",
1117
+ "select": "XNoGEa_select",
1118
+ "spacer": "XNoGEa_spacer",
1119
+ "status": "XNoGEa_status",
1120
+ "textarea": "XNoGEa_textarea"
1121
+ };
1122
+ //#endregion
1123
+ //#region src/client/CostPluginCard.tsx
1124
+ /**
1125
+ * Plugin configuration card (设置 → 插件 → cost-meter): the standard
1126
+ * DSH plugin-card shape (collapsible header, disclosure body, save/discard
1127
+ * footer) over the `cost-meter` settings namespace. Every editable
1128
+ * field binds the settings scope (`ctx.settingsScope.bind`) — the same
1129
+ * transport the shipped llm-deepseek / bash / web-search cards use — so the
1130
+ * card looks, stages, and persists like every other plugin configuration.
1131
+ * The JSON textareas carry the two structured fields (per-model price
1132
+ * overrides, OpenRouter aliases); the refresh button is a plain action.
1133
+ */
1134
+ const json = (value) => JSON.stringify(value ?? {}, null, 2);
1135
+ const CostPluginCard = (0, react.memo)(function CostPluginCard({ scope, t }) {
1136
+ const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
1137
+ const [open, setOpen] = (0, react.useState)(false);
1138
+ const [busy, setBusy] = (0, react.useState)(false);
1139
+ const [status, setStatus] = (0, react.useState)(null);
1140
+ const [error, setError] = (0, react.useState)(null);
1141
+ const [staged, setStaged] = (0, react.useState)(null);
1142
+ const [response, setResponse] = (0, react.useState)(null);
1143
+ const value = snapshot.value;
1144
+ (0, react.useEffect)(() => {
1145
+ setStaged({
1146
+ overridesText: json(value?.overrides),
1147
+ aliasesText: json(value?.aliases),
1148
+ cacheReadDiscount: String(value?.cacheReadDiscount ?? .25),
1149
+ fxMode: value?.fxMode ?? "auto",
1150
+ manualRate: String(value?.manualRate ?? 7.2),
1151
+ balanceEnabled: value?.balanceEnabled ?? true,
1152
+ openRouterEnabled: value?.openRouterEnabled ?? true
1153
+ });
1154
+ }, [value]);
1155
+ (0, react.useEffect)(() => {
1156
+ let alive = true;
1157
+ const load = () => {
1158
+ fetch("/cost-meter", { cache: "no-store" }).then((res) => res.ok ? res.json() : null).then((data) => {
1159
+ if (!alive || data === null) return;
1160
+ setResponse(data);
1161
+ }).catch(() => {});
1162
+ };
1163
+ load();
1164
+ return () => {
1165
+ alive = false;
1166
+ };
1167
+ }, []);
1168
+ const current = response?.pricebook.current ?? null;
1169
+ const source = current === null ? t("source.none") : t(`source.${current.source}`);
1170
+ const write = async (field, fieldValue) => {
1171
+ setBusy(true);
1172
+ setStatus(null);
1173
+ setError(null);
1174
+ try {
1175
+ await scope.set(field, fieldValue);
1176
+ setStatus(t("settings.saved"));
1177
+ } catch (cause) {
1178
+ setError(cause instanceof Error ? cause.message : String(cause));
1179
+ } finally {
1180
+ setBusy(false);
1181
+ }
1182
+ };
1183
+ const save = () => {
1184
+ if (staged === null) return;
1185
+ setError(null);
1186
+ let overrides;
1187
+ let aliases;
1188
+ try {
1189
+ overrides = JSON.parse(staged.overridesText);
1190
+ aliases = JSON.parse(staged.aliasesText);
1191
+ } catch (cause) {
1192
+ setError(cause instanceof Error ? cause.message : String(cause));
1193
+ return;
1194
+ }
1195
+ write("overrides", overrides);
1196
+ write("aliases", aliases);
1197
+ const discount = Number(staged.cacheReadDiscount);
1198
+ if (Number.isFinite(discount) && discount >= 0 && discount <= 1) write("cacheReadDiscount", discount);
1199
+ const rate = Number(staged.manualRate);
1200
+ if (Number.isFinite(rate) && rate > 0) write("manualRate", rate);
1201
+ if (staged.fxMode === "auto" || staged.fxMode === "manual") write("fxMode", staged.fxMode);
1202
+ write("balanceEnabled", staged.balanceEnabled);
1203
+ write("openRouterEnabled", staged.openRouterEnabled);
1204
+ };
1205
+ const refresh = () => {
1206
+ setBusy(true);
1207
+ setStatus(null);
1208
+ setError(null);
1209
+ fetch("/cost-meter", {
1210
+ method: "POST",
1211
+ headers: { "content-type": "application/json" },
1212
+ body: JSON.stringify({ action: "refresh" })
1213
+ }).then((res) => res.ok ? res.json() : null).then((data) => {
1214
+ if (data !== null) setResponse(data);
1215
+ setStatus(t("settings.saved"));
1216
+ }).catch((cause) => setError(cause instanceof Error ? cause.message : String(cause))).finally(() => setBusy(false));
1217
+ };
1218
+ const hint = current === null ? t("settings.noSnapshot") : `${t("settings.current", {
1219
+ source,
1220
+ time: formatTime(current.effectiveAt)
1221
+ })}`;
1222
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1223
+ className: CostPluginCard_module_css_default.card + (open ? ` ${CostPluginCard_module_css_default.cardOpen}` : ""),
1224
+ "data-testid": "cost-plugin-card",
1225
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1226
+ type: "button",
1227
+ className: CostPluginCard_module_css_default.header,
1228
+ onClick: () => setOpen((prev) => !prev),
1229
+ "aria-expanded": open,
1230
+ children: [
1231
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1232
+ className: CostPluginCard_module_css_default.headText,
1233
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1234
+ className: CostPluginCard_module_css_default.name,
1235
+ children: t("settings.title")
1236
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1237
+ className: CostPluginCard_module_css_default.description,
1238
+ children: hint
1239
+ })]
1240
+ }),
1241
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1242
+ className: CostPluginCard_module_css_default.pending,
1243
+ children: t("settings.error", { error })
1244
+ }),
1245
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: CostPluginCard_module_css_default.chevron + (open ? ` ${CostPluginCard_module_css_default.chevronOpen}` : "") })
1246
+ ]
1247
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1248
+ className: CostPluginCard_module_css_default.body,
1249
+ children: [
1250
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1251
+ className: CostPluginCard_module_css_default.field,
1252
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1253
+ className: CostPluginCard_module_css_default.head,
1254
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1255
+ className: CostPluginCard_module_css_default.label,
1256
+ children: t("settings.discount")
1257
+ })
1258
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1259
+ className: CostPluginCard_module_css_default.input,
1260
+ type: "number",
1261
+ min: 0,
1262
+ max: 1,
1263
+ step: .01,
1264
+ value: staged?.cacheReadDiscount ?? "",
1265
+ onChange: (event) => setStaged((prev) => prev === null ? prev : {
1266
+ ...prev,
1267
+ cacheReadDiscount: event.target.value
1268
+ })
1269
+ })]
1270
+ }),
1271
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1272
+ className: CostPluginCard_module_css_default.field,
1273
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1274
+ className: CostPluginCard_module_css_default.head,
1275
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1276
+ className: CostPluginCard_module_css_default.label,
1277
+ children: t("settings.fx")
1278
+ })
1279
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1280
+ className: CostPluginCard_module_css_default.inline,
1281
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1282
+ className: CostPluginCard_module_css_default.select,
1283
+ value: staged?.fxMode ?? "auto",
1284
+ onChange: (event) => setStaged((prev) => prev === null ? prev : {
1285
+ ...prev,
1286
+ fxMode: event.target.value
1287
+ }),
1288
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1289
+ value: "auto",
1290
+ children: t("settings.fx.auto")
1291
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1292
+ value: "manual",
1293
+ children: t("settings.fx.manual")
1294
+ })]
1295
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1296
+ className: CostPluginCard_module_css_default.input,
1297
+ type: "number",
1298
+ min: .001,
1299
+ step: .01,
1300
+ value: staged?.manualRate ?? "",
1301
+ onChange: (event) => setStaged((prev) => prev === null ? prev : {
1302
+ ...prev,
1303
+ manualRate: event.target.value
1304
+ })
1305
+ })]
1306
+ })]
1307
+ }),
1308
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1309
+ className: CostPluginCard_module_css_default.field,
1310
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1311
+ className: CostPluginCard_module_css_default.head,
1312
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1313
+ className: CostPluginCard_module_css_default.label,
1314
+ children: t("settings.balance")
1315
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1316
+ className: CostPluginCard_module_css_default.checkbox,
1317
+ type: "checkbox",
1318
+ checked: staged?.balanceEnabled ?? true,
1319
+ onChange: (event) => setStaged((prev) => prev === null ? prev : {
1320
+ ...prev,
1321
+ balanceEnabled: event.target.checked
1322
+ })
1323
+ })]
1324
+ })
1325
+ }),
1326
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1327
+ className: CostPluginCard_module_css_default.field,
1328
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1329
+ className: CostPluginCard_module_css_default.head,
1330
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1331
+ className: CostPluginCard_module_css_default.label,
1332
+ children: t("settings.openrouter")
1333
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1334
+ className: CostPluginCard_module_css_default.checkbox,
1335
+ type: "checkbox",
1336
+ checked: staged?.openRouterEnabled ?? true,
1337
+ onChange: (event) => setStaged((prev) => prev === null ? prev : {
1338
+ ...prev,
1339
+ openRouterEnabled: event.target.checked
1340
+ })
1341
+ })]
1342
+ })
1343
+ }),
1344
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1345
+ className: CostPluginCard_module_css_default.field,
1346
+ children: [
1347
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1348
+ className: CostPluginCard_module_css_default.head,
1349
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1350
+ className: CostPluginCard_module_css_default.label,
1351
+ children: t("settings.overrides")
1352
+ })
1353
+ }),
1354
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1355
+ className: CostPluginCard_module_css_default.textarea,
1356
+ value: staged?.overridesText ?? "",
1357
+ spellCheck: false,
1358
+ onChange: (event) => setStaged((prev) => prev === null ? prev : {
1359
+ ...prev,
1360
+ overridesText: event.target.value
1361
+ })
1362
+ }),
1363
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1364
+ className: CostPluginCard_module_css_default.hint,
1365
+ children: t("settings.jsonHint")
1366
+ })
1367
+ ]
1368
+ }),
1369
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1370
+ className: CostPluginCard_module_css_default.field,
1371
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1372
+ className: CostPluginCard_module_css_default.head,
1373
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1374
+ className: CostPluginCard_module_css_default.label,
1375
+ children: t("settings.aliases")
1376
+ })
1377
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1378
+ className: CostPluginCard_module_css_default.textarea,
1379
+ value: staged?.aliasesText ?? "",
1380
+ spellCheck: false,
1381
+ onChange: (event) => setStaged((prev) => prev === null ? prev : {
1382
+ ...prev,
1383
+ aliasesText: event.target.value
1384
+ })
1385
+ })]
1386
+ }),
1387
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1388
+ className: CostPluginCard_module_css_default.footer,
1389
+ children: [
1390
+ status !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1391
+ className: CostPluginCard_module_css_default.status,
1392
+ children: status
1393
+ }),
1394
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: CostPluginCard_module_css_default.spacer }),
1395
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1396
+ variant: "outline",
1397
+ onClick: () => setStaged(null),
1398
+ children: t("settings.discard")
1399
+ }),
1400
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1401
+ variant: "primary",
1402
+ onClick: save,
1403
+ disabled: busy || snapshot.status !== "ready",
1404
+ children: t("settings.save")
1405
+ }),
1406
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1407
+ variant: "outline",
1408
+ onClick: refresh,
1409
+ disabled: busy,
1410
+ children: busy ? t("settings.refreshing") : t("settings.refresh")
1411
+ })
1412
+ ]
1413
+ })
1414
+ ]
1415
+ })]
1416
+ });
1417
+ });
1418
+ //#endregion
1419
+ //#region src/client/index.ts
1420
+ /** The settings namespace this plugin owns (mirrors the host registration). */
1421
+ const SETTINGS_NAMESPACE = "cost-meter";
1422
+ /** Required services for locale registration, slot contributions, and the settings scope. */
1423
+ const inject = [
1424
+ "sessions",
1425
+ "slots",
1426
+ "locale",
1427
+ "settingsScope"
1428
+ ];
1429
+ /**
1430
+ * Client plugin body: register the dictionaries and the five slot entries.
1431
+ * @param ctx - client root context.
1432
+ */
1433
+ function apply(ctx) {
1434
+ ctx.effect(() => ctx.locale.register(NS, {
1435
+ zh,
1436
+ en
1437
+ }), "ui-cost-meter: dictionaries");
1438
+ const t = ctx.locale.bind(NS);
1439
+ const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE });
1440
+ ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
1441
+ name: "conversation.composer.dock",
1442
+ id: "cost-meter",
1443
+ order: 1,
1444
+ locale: NS
1445
+ }, SessionCostLine));
1446
+ ctx.slots.inject("conversation.view", () => ctx.slots.register({
1447
+ name: "conversation.view",
1448
+ id: "cost",
1449
+ order: 20,
1450
+ locale: NS,
1451
+ label: () => t("view.cost")
1452
+ }, CostView));
1453
+ ctx.slots.inject("conversation.chat.assistant-actions", () => ctx.slots.register({
1454
+ name: "conversation.chat.assistant-actions",
1455
+ id: "cost",
1456
+ order: 20,
1457
+ locale: NS
1458
+ }, AssistantCostChip));
1459
+ ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
1460
+ name: "conversation.session.header.utilities",
1461
+ id: "cost-meter",
1462
+ order: -10,
1463
+ locale: NS
1464
+ }, SessionCostPill));
1465
+ ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
1466
+ name: "settings.plugin.item",
1467
+ id: "cost-meter",
1468
+ order: 30,
1469
+ locale: NS,
1470
+ inject: () => ({ scope })
1471
+ }, CostPluginCard));
1472
+ }
1473
+ //#endregion
1474
+ exports.SETTINGS_NAMESPACE = SETTINGS_NAMESPACE;
1475
+ exports.apply = apply;
1476
+ exports.inject = inject;
1477
+ return module.exports;
1478
+ }
1479
+ });
1480
+
1481
+ //# sourceMappingURL=client.js.map