@cassius0924/dsh-usage-dashboard 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -131,7 +131,7 @@ async function getJson(path, cache) {
131
131
  var usageIsUsable = (res) => {
132
132
  if (res.ok !== true) return false;
133
133
  const data = res.data;
134
- return data !== void 0 && Array.isArray(data.daily) && Array.isArray(data.hourly) && Array.isArray(data.heatmap) && Array.isArray(data.models) && Array.isArray(data.sessions) && typeof data.sessionCount === "number" && data.coverage?.scope === "local-dsh-session-logs" && typeof data.coverage.scannedSessions === "number" && typeof data.coverage.skippedRecords === "number" && Array.isArray(data.windows) && data.windows.length === USAGE_WINDOW_DAYS.length && USAGE_WINDOW_DAYS.every((days) => data.windows.some((window2) => window2.days === days)) && data.windows.every((window2) => Array.isArray(window2.daily) && Array.isArray(window2.hourly) && Array.isArray(window2.models) && Array.isArray(window2.sessions)) && data.totals !== void 0 && typeof data.totals.cacheSavings === "number" && data.summary?.today !== void 0 && Array.isArray(data.pricing?.tiers) && data.peakSplit?.peak !== void 0;
134
+ return data !== void 0 && Array.isArray(data.daily) && Array.isArray(data.hourly) && Array.isArray(data.heatmap) && Array.isArray(data.models) && Array.isArray(data.sessions) && typeof data.sessionCount === "number" && data.coverage?.scope === "local-dsh-session-logs" && typeof data.coverage.scannedSessions === "number" && typeof data.coverage.skippedRecords === "number" && Array.isArray(data.windows) && data.windows.length === USAGE_WINDOW_DAYS.length && USAGE_WINDOW_DAYS.every((days) => data.windows.some((window2) => window2.days === days)) && data.windows.every((window2) => Array.isArray(window2.daily) && Array.isArray(window2.hourly) && Array.isArray(window2.models) && Array.isArray(window2.sessions)) && data.vision !== void 0 && typeof data.vision.images === "number" && typeof data.vision.imageTokens === "number" && typeof data.vision.cost === "number" && Array.isArray(data.visionDaily) && Array.isArray(data.visionSessions) && data.windows.every((window2) => window2.vision !== void 0 && Array.isArray(window2.visionDaily) && Array.isArray(window2.visionSessions)) && data.totals !== void 0 && typeof data.totals.cacheSavings === "number" && data.summary?.today !== void 0 && Array.isArray(data.pricing?.tiers) && data.peakSplit?.peak !== void 0;
135
135
  };
136
136
  var balanceIsUsable = (res) => res.ok === true && Array.isArray(res.data?.balances);
137
137
  var balanceCache = createCache(BALANCE_TTL_MS, "dsh-usage-dashboard:balance", balanceIsUsable);
@@ -174,8 +174,8 @@ async function fetchUsage(force = false) {
174
174
  if (usageInflight !== null) return usageInflight;
175
175
  }
176
176
  const suffix = force ? "?refresh=1" : "";
177
- const task = getJson(`/api/dsh-usage-dashboard/usage${suffix}`, force ? "no-store" : "default").then((res) => {
178
- if (res.ok) {
177
+ const task = getJson(`/api/dsh-usage-dashboard/usage${suffix}`, "no-store").then((res) => {
178
+ if (res.ok && usageIsUsable(res)) {
179
179
  usageCache.put(res);
180
180
  for (const listener of [...usageListeners]) listener();
181
181
  }
@@ -190,6 +190,26 @@ async function fetchUsage(force = false) {
190
190
  return task;
191
191
  }
192
192
  var sessionUsageInflight = /* @__PURE__ */ new Map();
193
+ var sessionUsageCache = /* @__PURE__ */ new Map();
194
+ var sessionUsageListeners = /* @__PURE__ */ new Map();
195
+ var getCachedSessionUsage = (sessionId) => sessionUsageCache.get(sessionId) ?? null;
196
+ var subscribeSessionUsage = (sessionId, fn) => {
197
+ let listeners = sessionUsageListeners.get(sessionId);
198
+ if (listeners === void 0) {
199
+ listeners = /* @__PURE__ */ new Set();
200
+ sessionUsageListeners.set(sessionId, listeners);
201
+ }
202
+ listeners.add(fn);
203
+ return () => {
204
+ listeners?.delete(fn);
205
+ if (listeners?.size === 0) sessionUsageListeners.delete(sessionId);
206
+ };
207
+ };
208
+ var publishSessionUsage = (sessionId, response) => {
209
+ if (!response.ok) return;
210
+ sessionUsageCache.set(sessionId, response);
211
+ for (const listener of [...sessionUsageListeners.get(sessionId) ?? []]) listener();
212
+ };
193
213
  async function fetchSessionUsage(sessionId, force = false) {
194
214
  if (typeof sessionId !== "string" || sessionId === "") {
195
215
  return { ok: false, error: "\u7F3A\u5C11\u4F1A\u8BDD id" };
@@ -198,7 +218,10 @@ async function fetchSessionUsage(sessionId, force = false) {
198
218
  const inflight = sessionUsageInflight.get(sessionId);
199
219
  if (inflight !== void 0) return inflight;
200
220
  }
201
- const task = getJson(`/api/dsh-usage-dashboard/session?id=${encodeURIComponent(sessionId)}`, "no-store");
221
+ const task = getJson(`/api/dsh-usage-dashboard/session?id=${encodeURIComponent(sessionId)}`, "no-store").then((response) => {
222
+ publishSessionUsage(sessionId, response);
223
+ return response;
224
+ });
202
225
  if (force) return task;
203
226
  const tracked = task.finally(() => {
204
227
  if (sessionUsageInflight.get(sessionId) === tracked) sessionUsageInflight.delete(sessionId);
@@ -309,16 +332,15 @@ var zh = {
309
332
  "pricing.currentOffPeak": "\u5F53\u524D \u95F2\u65F6",
310
333
  "pricing.model": "\u6A21\u578B",
311
334
  "pricing.period": "\u65F6\u6BB5",
312
- "pricing.unitPrice": "\u5355\u4EF7",
313
335
  "pricing.inputCacheHit": "\u8F93\u5165\xB7\u7F13\u5B58\u547D\u4E2D",
314
336
  "pricing.inputCacheMiss": "\u8F93\u5165\xB7\u672A\u547D\u4E2D",
315
337
  "pricing.output": "\u8F93\u51FA",
316
338
  "pricing.peak": "\u9AD8\u5CF0",
317
- "pricing.fixed": "\u56FA\u5B9A",
318
339
  "pricing.offPeak": "\u95F2\u65F6",
319
340
  "pricing.unit": "\u5355\u4F4D\uFF1A{currency} / \u767E\u4E07 tokens\u3002",
320
- "pricing.splitNote": "\u9AD8\u5CF0\u65F6\u6BB5\u4E3A\u5317\u4EAC\u65F6\u95F4 {windows}\uFF0C\u5176\u4F59\u4E3A\u95F2\u65F6\uFF08\u4EF7\u683C\u51CF\u534A\uFF09\uFF1B{date} \u4E4B\u524D\u7684\u7528\u91CF\u4ECD\u6309\u65E7\u4EF7\u4F30\u7B97\u3002",
321
- "pricing.switchNote": "{date} 00:00 \u8D77\u6539\u4E3A\u5CF0\u8C37\u5B9A\u4EF7\uFF08\u9AD8\u5CF0 {windows}\uFF0C\u95F2\u65F6\u4EF7\u683C\u51CF\u534A\uFF09\uFF0C\u5C4A\u65F6\u672C\u9875\u4F1A\u6309\u6BCF\u6761\u8BB0\u5F55\u7684\u65F6\u95F4\u81EA\u52A8\u5206\u6BB5\u8BA1\u4EF7\u3002",
341
+ "pricing.splitNote": "\u9AD8\u5CF0\u65F6\u6BB5\u4E3A\u5317\u4EAC\u65F6\u95F4 {windows}\uFF08\u5468\u4E00\u81F3\u5468\u4E94\uFF09\uFF0C\u5176\u4F59\u4E3A\u95F2\u65F6\uFF08\u4EF7\u683C\u51CF\u534A\uFF09\uFF1B\u6BCF\u6761\u7528\u91CF\u6309\u53D1\u751F\u65F6\u523B\u751F\u6548\u7684\u5B98\u65B9\u4EF7\u683C\u4F30\u7B97\u3002",
342
+ "pricing.visionNote": "\u56FE\u7247\u6309\u5C3A\u5BF8\u6298\u7B97 token\uFF0C\u4E0E\u6587\u672C\u4E00\u5E76\u8BA1\u8D39\u3002",
343
+ "pricing.modelNote": "deepseek-v4-flash\u3001deepseek-v4-flash-vision-exp \u662F\u65E7\u6A21\u578B\u540D\uFF0C\u73B0\u7531 deepseek-flash \u627F\u63A5\u5E76\u6309 Flash \u4EF7\u8BA1\u8D39\uFF1B2026-09-14 12:00 \u8D77 deepseek-v4-pro \u7684\u8BF7\u6C42\u4E5F\u4F1A\u8DEF\u7531\u5230 V4.1-Flash\uFF0C\u6309 Flash \u4EF7\u8BA1\u8D39\u3002",
322
344
  "pricing.unknown": "\u672A\u77E5\u6A21\u578B\u6309 deepseek-v4-pro \u8BA1\u4EF7\u3002",
323
345
  "coverage.noRange": "\u6682\u65E0\u6709\u6548\u7528\u91CF\u65F6\u95F4",
324
346
  "coverage.title": "\u7EDF\u8BA1\u8303\u56F4",
@@ -352,13 +374,23 @@ var zh = {
352
374
  "peak.aria": "\u9AD8\u5CF0\u65F6\u6BB5 {peak}%\uFF0C\u95F2\u65F6 {offPeak}%",
353
375
  "peak.peakLegend": "\u9AD8\u5CF0 {share}% \xB7 {tokens} tokens \xB7 {calls}",
354
376
  "peak.offPeakLegend": "\u95F2\u65F6 {share}% \xB7 {tokens} tokens \xB7 {calls}",
355
- "peak.newPrice": "\u540C\u6837\u7528\u91CF\u5728\u65B0\u4EF7\u4E0B",
356
- "peak.increase": "\u8F83\u73B0\u4EF7 +{percent}%",
357
377
  "peak.shift": "\u82E5\u9AD8\u5CF0\u7528\u91CF\u90FD\u632A\u5230\u95F2\u65F6",
358
378
  "peak.saving": "\u53EF\u7701 \xA5{amount}",
359
- "peak.schedule": "\u9AD8\u5CF0\u65F6\u6BB5\u4E3A\u5317\u4EAC\u65F6\u95F4 {windows}\uFF0C\u95F2\u65F6\u4EF7\u683C\u51CF\u534A\u3002",
379
+ "peak.schedule": "\u9AD8\u5CF0\u65F6\u6BB5\u4E3A\u5317\u4EAC\u65F6\u95F4 {windows}\uFF08\u5468\u4E00\u81F3\u5468\u4E94\uFF09\uFF0C\u95F2\u65F6\u4EF7\u683C\u51CF\u534A\u3002",
360
380
  "peak.activeHint": "\u628A\u6279\u91CF\u3001\u53EF\u5EF6\u540E\u7684\u4EFB\u52A1\u653E\u5230\u95F2\u65F6\u8DD1\uFF0C\u540C\u6837\u7684 token \u53EA\u8981\u4E00\u534A\u7684\u94B1\u3002",
361
- "peak.futureHint": "\u65B0\u4EF7 {date} 00:00 \u751F\u6548\uFF1B\u4E0A\u9762\u4E24\u4E2A\u6570\u5B57\u662F\u6309\u4F60\u5DF2\u6709\u7684\u5168\u90E8\u7528\u91CF\u91CD\u7B97\u7684\u3002",
381
+ "vision.empty": "{window}\u6CA1\u6709\u56FE\u7247\u8F93\u5165\u3002",
382
+ "vision.title": "\u56FE\u7247 / \u591A\u6A21\u6001\u7528\u91CF",
383
+ "vision.images": "\u56FE\u7247\u5F20\u6570",
384
+ "vision.tokens": "\u4F30\u7B97\u56FE\u7247 tokens",
385
+ "vision.cost": "\u4F30\u7B97\u8D39\u7528",
386
+ "vision.tokensShare": "\u5360\u8F93\u5165 {percent}%",
387
+ "vision.costShare": "\u5360\u8D39\u7528 {percent}%",
388
+ "vision.note": "\u56FE\u7247\u6309\u5B98\u65B9\u6362\u7B97\u89C4\u5219\u4F30\u7B97\uFF1A\u5148\u6309\u6BD4\u4F8B\u7F29\u653E\uFF08\u4E0A\u9650 ~800\xD7800\u3001\u4E0B\u9650 384\xD7384\uFF09\uFF0Ctoken \u4E0E\u7F29\u653E\u540E\u9762\u79EF\u6210\u6B63\u6BD4\u3001\u6BCF\u5F20\u4E0A\u9650 384\uFF1B\u56FE\u7247\u5F52\u5C5E\u5230\u968F\u540E\u4E00\u6B21\u6A21\u578B\u8C03\u7528\uFF0C\u6309\u8BE5\u8C03\u7528\u7684\u65F6\u6BB5\u4E0E\u6A21\u578B\u5355\u4EF7\u4F30\u7B97\uFF0C\u5B9E\u9645\u4EE5\u63A5\u53E3\u8FD4\u56DE\u7684\u7528\u91CF\u4E3A\u51C6\u3002",
389
+ "vision.sessionsTitle": "\u56FE\u7247\u6700\u591A\u7684\u4F1A\u8BDD",
390
+ "vision.sessionRow": "{images} \u5F20 \xB7 \xA5{cost}",
391
+ "vision.dailyTitle": "{window} \xB7 \u6BCF\u5929\u56FE\u7247\u6570",
392
+ "vision.barAria": "{date} \xB7 {images} \u5F20\u56FE\u7247 \xB7 \u4F30\u7B97 {tokens} tokens",
393
+ "vision.barTitle": "{date} \xB7 {images} \u5F20\u56FE\u7247 \xB7 \u4F30\u7B97 {tokens} tokens \xB7 \xA5{cost}",
362
394
  "sessions.empty": "\u8FD8\u6CA1\u6709\u4EA7\u751F\u8D39\u7528\u7684\u4F1A\u8BDD\u3002",
363
395
  "sessions.detail": "{tokens} tokens \xB7 {calls} \xB7 {ago}",
364
396
  "sessions.share": "\u5360 {percent}%",
@@ -484,7 +516,10 @@ var zh = {
484
516
  "links.status": "\u670D\u52A1\u72B6\u6001",
485
517
  "settings.title": "\u8BBE\u7F6E",
486
518
  "settings.widget": "\u5728\u5176\u4ED6\u9875\u9762\u663E\u793A\u53F3\u4E0B\u89D2\u60AC\u6D6E\u989D\u5EA6\u7A97\u53E3",
487
- "settings.widgetHint": "\u5F53\u524D\u989D\u5EA6\u9875\u5DF2\u5C55\u793A\u5B8C\u6574\u6570\u636E\uFF0C\u60AC\u6D6E\u7A97\u4F1A\u81EA\u52A8\u9690\u85CF\uFF1B\u5207\u56DE Chat \u6216 Trajectory \u540E\u6062\u590D\u3002",
519
+ "settings.widgetHint": "\u989D\u5EA6\u9875\u4F1A\u81EA\u52A8\u9690\u85CF\u6D6E\u7A97\uFF1B\u5176\u4ED6\u9875\u9762\u662F\u5426\u5C55\u793A\u7531\u4E0B\u65B9\u9009\u62E9\u51B3\u5B9A\u3002",
520
+ "settings.widgetTabs": "\u5C55\u793A\u6D6E\u7A97\u7684\u9875\u9762",
521
+ "settings.widgetTabsHint": "\u9875\u9762\u5217\u8868\u6765\u81EA DSH \u7684\u5B9E\u65F6 Tab \u6CE8\u518C\u8868\uFF1B\u5176\u4ED6\u63D2\u4EF6\u65B0\u589E\u7684 Tab \u4E5F\u4F1A\u81EA\u52A8\u51FA\u73B0\u5728\u8FD9\u91CC\u3002",
522
+ "settings.widgetTabsEmpty": "\u5F53\u524D\u6CA1\u6709\u53EF\u914D\u7F6E\u7684\u4F1A\u8BDD\u9875\u9762\u3002",
488
523
  "settings.lowBalance": "\u4F59\u989D\u9884\u8B66\u7EBF",
489
524
  "settings.lowBalanceOn": "\u4F59\u989D\u4F4E\u4E8E {amount} {currency} \u65F6\uFF0C\u8FD9\u91CC\u548C\u60AC\u6D6E\u7A97\u90FD\u4F1A\u8F6C\u4E3A\u8B66\u793A\u8272\u3002\u586B 0 \u5173\u95ED\u3002",
490
525
  "settings.lowBalanceOff": "\u5DF2\u5173\u95ED\u4F59\u989D\u9884\u8B66\u3002\u586B\u4E00\u4E2A\u5927\u4E8E 0 \u7684\u6570\u5F00\u542F\u3002",
@@ -497,6 +532,8 @@ var zh = {
497
532
  "widget.lowTitle": "\u4F59\u989D\u4F4E\u4E8E\u9884\u8B66\u7EBF {amount}",
498
533
  "widget.todayCostTitle": "\u4ECA\u65E5\u6D88\u8017\uFF08\u4F30\u7B97\uFF09\xB7 {age}",
499
534
  "widget.todayCost": "\u4ECA\u65E5\u6D88\u8017",
535
+ "widget.sessionCostTitle": "\u5F53\u524D\u4F1A\u8BDD\u7D2F\u8BA1\u6D88\u8017\uFF08\u4F30\u7B97\uFF09",
536
+ "widget.sessionCost": "\u5F53\u524D\u4F1A\u8BDD",
500
537
  "widget.noBalance": "\u6682\u65E0\u4F59\u989D\u6570\u636E",
501
538
  "widget.dragCard": "\u62D6\u52A8\u5230\u56DB\u4E2A\u89D2\u843D",
502
539
  "widget.dragGrip": "\u62D6\u52A8\u4EE5\u79FB\u52A8",
@@ -505,6 +542,9 @@ var zh = {
505
542
  "widget.collapse": "\u6536\u8D77",
506
543
  "widget.expandAria": "\u5C55\u5F00\u989D\u5EA6\u7A97\u53E3",
507
544
  "widget.collapseAria": "\u6536\u8D77\u989D\u5EA6\u7A97\u53E3",
545
+ "messageCost.label": "\u672C\u8F6E\u8D39\u7528",
546
+ "messageCost.tooltip": "\u7531 dsh-usage-dashboard \u63D0\u4F9B \xB7 \u672C\u8F6E\u5BF9\u8BDD\u8D39\u7528\u4E3A\u4F30\u7B97\u503C",
547
+ "messageCost.aria": "{amount}\uFF0C\u672C\u8F6E\u5BF9\u8BDD\u4F30\u7B97\u8D39\u7528\uFF0C\u7531 dsh-usage-dashboard \u63D0\u4F9B",
508
548
  "boundary.title": "\u989D\u5EA6\u9762\u677F\u51FA\u9519\u4E86",
509
549
  "boundary.body": "\u6E32\u67D3\u65F6\u629B\u4E86\u5F02\u5E38\uFF0C\u901A\u5E38\u662F\u6D4F\u89C8\u5668\u91CC\u5B58\u7740\u65E7\u7248\u672C\u7684\u7F13\u5B58\u6570\u636E\u3002\u6E05\u6389\u672C\u63D2\u4EF6\u7684\u672C\u5730\u6570\u636E\u518D\u91CD\u65B0\u52A0\u8F7D\u5C31\u80FD\u6062\u590D\u3002",
510
550
  "boundary.reset": "\u6E05\u9664\u672C\u5730\u6570\u636E\u5E76\u91CD\u65B0\u52A0\u8F7D"
@@ -550,16 +590,15 @@ var en = {
550
590
  "pricing.currentOffPeak": "Off-peak now",
551
591
  "pricing.model": "Model",
552
592
  "pricing.period": "Period",
553
- "pricing.unitPrice": "Rate",
554
593
  "pricing.inputCacheHit": "Input \xB7 cache hit",
555
594
  "pricing.inputCacheMiss": "Input \xB7 cache miss",
556
595
  "pricing.output": "Output",
557
596
  "pricing.peak": "Peak",
558
- "pricing.fixed": "Fixed",
559
597
  "pricing.offPeak": "Off-peak",
560
598
  "pricing.unit": "Unit: {currency} / million tokens. ",
561
- "pricing.splitNote": "Peak periods are {windows} Beijing time; off-peak rates are half price. Usage before {date} is still estimated at the old rates. ",
562
- "pricing.switchNote": "Time-of-use pricing starts at 00:00 on {date} (peak: {windows}; off-peak is half price). This page will price each record by its timestamp. ",
599
+ "pricing.splitNote": "Peak periods are {windows} Beijing time, Monday to Friday; off-peak rates are half price. Each record is estimated at the official rates in force when it was written. ",
600
+ "pricing.visionNote": "Images are converted to tokens by size and charged together with text. ",
601
+ "pricing.modelNote": "deepseek-v4-flash and deepseek-v4-flash-vision-exp are legacy names, now served by deepseek-flash at Flash rates; from 2026-09-14 12:00 deepseek-v4-pro requests are routed to V4.1-Flash and billed at Flash rates. ",
563
602
  "pricing.unknown": "Unknown models use deepseek-v4-pro rates.",
564
603
  "coverage.noRange": "No valid usage timestamps",
565
604
  "coverage.title": "Coverage",
@@ -593,13 +632,23 @@ var en = {
593
632
  "peak.aria": "{peak}% peak, {offPeak}% off-peak",
594
633
  "peak.peakLegend": "Peak {share}% \xB7 {tokens} tokens \xB7 {calls}",
595
634
  "peak.offPeakLegend": "Off-peak {share}% \xB7 {tokens} tokens \xB7 {calls}",
596
- "peak.newPrice": "Same usage at new rates",
597
- "peak.increase": "+{percent}% vs current rates",
598
635
  "peak.shift": "If all peak usage moved off-peak",
599
636
  "peak.saving": "Save \xA5{amount}",
600
- "peak.schedule": "Peak periods are {windows} Beijing time; off-peak rates are half price. ",
637
+ "peak.schedule": "Peak periods are {windows} Beijing time, Monday to Friday; off-peak rates are half price. ",
601
638
  "peak.activeHint": "Run batch or delay-tolerant work off-peak to pay half as much for the same tokens.",
602
- "peak.futureHint": "New rates start at 00:00 on {date}; both figures recalculate all existing usage.",
639
+ "vision.empty": "No image input in {window}.",
640
+ "vision.title": "Image / multimodal usage",
641
+ "vision.images": "Images",
642
+ "vision.tokens": "Estimated image tokens",
643
+ "vision.cost": "Estimated cost",
644
+ "vision.tokensShare": "{percent}% of input",
645
+ "vision.costShare": "{percent}% of cost",
646
+ "vision.note": "Images are estimated with the official rule: resized keeping aspect ratio (cap ~800\xD7800, floor 384\xD7384), tokens proportional to the resized area with a 384-token cap per image. Each image is attributed to the next model call and priced at that call's period and model rate; the provider usage numbers remain the source of truth.",
647
+ "vision.sessionsTitle": "Sessions with the most images",
648
+ "vision.sessionRow": "{images} images \xB7 \xA5{cost}",
649
+ "vision.dailyTitle": "{window} \xB7 images per day",
650
+ "vision.barAria": "{date} \xB7 {images} images \xB7 est. {tokens} tokens",
651
+ "vision.barTitle": "{date} \xB7 {images} images \xB7 est. {tokens} tokens \xB7 \xA5{cost}",
603
652
  "sessions.empty": "No sessions have incurred cost yet.",
604
653
  "sessions.detail": "{tokens} tokens \xB7 {calls} \xB7 {ago}",
605
654
  "sessions.share": "{percent}% of total",
@@ -725,7 +774,10 @@ var en = {
725
774
  "links.status": "Service status",
726
775
  "settings.title": "Settings",
727
776
  "settings.widget": "Show the floating balance widget on other views",
728
- "settings.widgetHint": "The widget hides while this full dashboard is open, then returns on Chat or Trajectory.",
777
+ "settings.widgetHint": "The widget always hides on Usage; choose its other views below.",
778
+ "settings.widgetTabs": "Show widget on",
779
+ "settings.widgetTabsHint": "This live list comes from DSH\u2019s tab registry, including tabs added by other plugins.",
780
+ "settings.widgetTabsEmpty": "There are no configurable conversation views right now.",
729
781
  "settings.lowBalance": "Balance warning",
730
782
  "settings.lowBalanceOn": "This page and the widget turn amber below {amount} {currency}. Enter 0 to disable.",
731
783
  "settings.lowBalanceOff": "Balance warnings are off. Enter an amount above 0 to enable them.",
@@ -738,6 +790,8 @@ var en = {
738
790
  "widget.lowTitle": "Balance below warning threshold {amount}",
739
791
  "widget.todayCostTitle": "Today\u2019s spend (estimated) \xB7 {age}",
740
792
  "widget.todayCost": "Today\u2019s spend",
793
+ "widget.sessionCostTitle": "Estimated spend for the current session",
794
+ "widget.sessionCost": "Current session",
741
795
  "widget.noBalance": "No balance data",
742
796
  "widget.dragCard": "Drag to any corner",
743
797
  "widget.dragGrip": "Drag to move",
@@ -746,6 +800,9 @@ var en = {
746
800
  "widget.collapse": "Collapse",
747
801
  "widget.expandAria": "Expand balance widget",
748
802
  "widget.collapseAria": "Collapse balance widget",
803
+ "messageCost.label": "Turn cost",
804
+ "messageCost.tooltip": "Provided by dsh-usage-dashboard \xB7 estimated cost for this turn",
805
+ "messageCost.aria": "{amount}, estimated cost for this turn, provided by dsh-usage-dashboard",
749
806
  "boundary.title": "The usage dashboard crashed",
750
807
  "boundary.body": "Rendering failed, usually because this browser still has cached data from an older version. Clear this plugin\u2019s local data and reload to recover.",
751
808
  "boundary.reset": "Clear local data and reload"
@@ -1098,9 +1155,34 @@ function getShellFrame(node) {
1098
1155
  return overlay?.parentElement ?? null;
1099
1156
  }
1100
1157
  function getComposerElement(frame) {
1101
- if (frame === null) return null;
1158
+ if (frame === null) {
1159
+ return slotElement(document.documentElement, "conversation.composer.dock", "ancestor");
1160
+ }
1102
1161
  return slotElement(frame, "conversation.composer.dock", "ancestor");
1103
1162
  }
1163
+ function getComposerInputElement(frame) {
1164
+ const root = frame ?? document.documentElement;
1165
+ const textarea = root.querySelector(
1166
+ '[data-slot="conversation.composer"] textarea, [data-slot="conversation.composer.bar"] textarea, textarea'
1167
+ );
1168
+ if (textarea === null) return null;
1169
+ let el = textarea.parentElement;
1170
+ for (let depth = 0; depth < 6 && el !== null; depth++) {
1171
+ const cs = getComputedStyle(el);
1172
+ if (cs.borderRadius !== "0px" && cs.backgroundColor !== "rgba(0, 0, 0, 0)") return el;
1173
+ el = el.parentElement;
1174
+ }
1175
+ return null;
1176
+ }
1177
+ function getActiveConversationViewId(frame, viewIds) {
1178
+ if (viewIds.length === 0) return null;
1179
+ const header = frame === null ? null : slotElement(frame, "conversation.session.header", "child");
1180
+ const tablist = header?.querySelector('[role="tablist"]') ?? null;
1181
+ if (tablist === null) return viewIds[0] ?? null;
1182
+ const tabs = [...tablist.querySelectorAll('[role="tab"]')];
1183
+ const activeIndex = tabs.findIndex((tab) => tab.getAttribute("aria-selected") === "true");
1184
+ return activeIndex < 0 ? viewIds[0] ?? null : viewIds[activeIndex] ?? null;
1185
+ }
1104
1186
 
1105
1187
  // src/client/export.ts
1106
1188
  var BEIJING_OFFSET_MS2 = 8 * 60 * 60 * 1e3;
@@ -1240,6 +1322,8 @@ function createStore(key, isValid, fallback) {
1240
1322
  }
1241
1323
  var isThreshold = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
1242
1324
  var widgetVisibleStore = createStore("widget.visible", isBoolean, true);
1325
+ var isWidgetTabIds = (value) => value === null || Array.isArray(value) && value.every((id) => typeof id === "string" && id !== "") && new Set(value).size === value.length;
1326
+ var widgetTabIdsStore = createStore("widget.tabIds", isWidgetTabIds, null);
1243
1327
  var lowBalanceStore = createStore("alert.lowBalance", isThreshold, 10);
1244
1328
  var monthlyBudgetStore = createStore("budget.monthly", isThreshold, 0);
1245
1329
  var isUsageWindowDays = (value) => typeof value === "number" && USAGE_WINDOW_DAYS.some((days) => days === value);
@@ -1393,30 +1477,29 @@ var rate = (value) => `\xA5${value.toLocaleString(void 0, { maximumFractionDigit
1393
1477
  function PricingNote(props) {
1394
1478
  const { t } = useI18n();
1395
1479
  const { pricing } = props;
1396
- const split = pricing.splitActive;
1397
1480
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("details", { className: "dq-pricing", children: [
1398
1481
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("summary", { className: "dq-pricing-summary", children: [
1399
1482
  t("pricing.title"),
1400
- split && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: `dq-pricing-now${pricing.inPeakNow ? " dq-pricing-now--peak" : ""}`, children: pricing.inPeakNow ? t("pricing.currentPeak") : t("pricing.currentOffPeak") })
1483
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: `dq-pricing-now${pricing.inPeakNow ? " dq-pricing-now--peak" : ""}`, children: pricing.inPeakNow ? t("pricing.currentPeak") : t("pricing.currentOffPeak") })
1401
1484
  ] }),
1402
1485
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-pricing-body", children: [
1403
1486
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("table", { className: "dq-pricing-table", children: [
1404
1487
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
1405
1488
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: t("pricing.model") }),
1406
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: split ? t("pricing.period") : t("pricing.unitPrice") }),
1489
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: t("pricing.period") }),
1407
1490
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: t("pricing.inputCacheHit") }),
1408
1491
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: t("pricing.inputCacheMiss") }),
1409
1492
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: t("pricing.output") })
1410
1493
  ] }) }),
1411
1494
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("tbody", { children: pricing.tiers.map((tier) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react4.Fragment, { children: [
1412
1495
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
1413
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { rowSpan: tier.offPeak !== null ? 2 : 1, children: tier.model }),
1414
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: split ? t("pricing.peak") : t("pricing.fixed") }),
1496
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { rowSpan: 2, children: tier.model }),
1497
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: t("pricing.peak") }),
1415
1498
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.peak.cacheHit) }),
1416
1499
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.peak.input) }),
1417
1500
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.peak.output) })
1418
1501
  ] }),
1419
- tier.offPeak !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
1502
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
1420
1503
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: t("pricing.offPeak") }),
1421
1504
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.offPeak.cacheHit) }),
1422
1505
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.offPeak.input) }),
@@ -1426,7 +1509,9 @@ function PricingNote(props) {
1426
1509
  ] }),
1427
1510
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { className: "dq-pricing-foot", children: [
1428
1511
  t("pricing.unit", { currency: pricing.currency }),
1429
- split ? t("pricing.splitNote", { windows: pricing.peakWindows.join(t("common.listSeparator")), date: pricing.switchDate }) : t("pricing.switchNote", { windows: pricing.peakWindows.join(t("common.listSeparator")), date: pricing.switchDate }),
1512
+ t("pricing.splitNote", { windows: pricing.peakWindows.join(t("common.listSeparator")) }),
1513
+ t("pricing.visionNote"),
1514
+ t("pricing.modelNote"),
1430
1515
  t("pricing.unknown")
1431
1516
  ] })
1432
1517
  ] })
@@ -1678,15 +1763,50 @@ function CacheCard(props) {
1678
1763
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-cache-foot", children: low ? t("cache.lowHint") : t("cache.goodHint") })
1679
1764
  ] });
1680
1765
  }
1766
+ function VisionCard(props) {
1767
+ const { t, locale } = useI18n();
1768
+ const { vision, daily, sessions, windowLabel, inputTokens, costTotal } = props;
1769
+ if (vision.images === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("vision.empty", { window: windowLabel }) });
1770
+ const tokensShare = inputTokens > 0 ? vision.imageTokens / inputTokens * 100 : 0;
1771
+ const costShare = costTotal > 0 ? vision.cost / costTotal * 100 : 0;
1772
+ const labelEvery = daily.length <= 7 ? 1 : daily.length <= 30 ? 5 : daily.length <= 90 ? 15 : 30;
1773
+ const bars = daily.map((d) => ({
1774
+ label: d.date.slice(8, 10),
1775
+ value: d.images,
1776
+ title: t("vision.barTitle", { date: d.date, images: fmtInt(d.images), tokens: fmtCompact(d.imageTokens, locale), cost: fmt(d.cost) })
1777
+ }));
1778
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1779
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-usage-totals", children: [
1780
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("vision.images"), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-value", children: fmtInt(vision.images) }) }),
1781
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("vision.tokens"), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat-value", children: [
1782
+ fmtCompact(vision.imageTokens, locale),
1783
+ tokensShare > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-vision-share", children: t("vision.tokensShare", { percent: tokensShare.toFixed(1) }) })
1784
+ ] }) }),
1785
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("vision.cost"), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat-value", children: [
1786
+ "\xA5 ",
1787
+ fmt(vision.cost),
1788
+ costShare > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-vision-share", children: t("vision.costShare", { percent: costShare.toFixed(1) }) })
1789
+ ] }) })
1790
+ ] }),
1791
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-chart-title", children: t("vision.dailyTitle", { window: windowLabel }) }),
1792
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Bars, { data: bars, height: 80, labelEvery }),
1793
+ sessions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-vision-sessions", children: [
1794
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-chart-title", children: t("vision.sessionsTitle") }),
1795
+ sessions.map((session) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-session", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-session-head", children: [
1796
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-session-title", title: session.id, children: session.title }),
1797
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-session-cost", children: t("vision.sessionRow", { images: fmtInt(session.images), cost: fmt(session.cost) }) })
1798
+ ] }) }, session.id))
1799
+ ] }),
1800
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-cache-foot", children: t("vision.note") })
1801
+ ] });
1802
+ }
1681
1803
  function PeakCard(props) {
1682
1804
  const { t, locale } = useI18n();
1683
- const { split, pricing, currentCost } = props;
1805
+ const { split, pricing } = props;
1684
1806
  const total = split.peak.total + split.offPeak.total;
1685
1807
  if (total === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("peak.empty") });
1686
1808
  const peakShare = split.peak.total / total;
1687
1809
  const shiftSaving = split.peakEraCost - split.offPeakEraCost;
1688
- const increase = split.peakEraCost - currentCost;
1689
- const increasePercent = currentCost > 0 ? increase / currentCost * 100 : 0;
1690
1810
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-peak", children: [
1691
1811
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1692
1812
  "div",
@@ -1715,27 +1835,17 @@ function PeakCard(props) {
1715
1835
  })
1716
1836
  ] })
1717
1837
  ] }),
1718
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-peak-figures", children: [
1719
- !pricing.splitActive && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1720
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: t("peak.newPrice") }),
1721
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat-value", children: [
1722
- "\xA5 ",
1723
- fmt(split.peakEraCost),
1724
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-peak-delta", children: t("peak.increase", { percent: increasePercent.toFixed(0) }) })
1725
- ] })
1726
- ] }),
1727
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1728
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: t("peak.shift") }),
1729
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat-value", children: [
1730
- "\xA5 ",
1731
- fmt(split.offPeakEraCost),
1732
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-peak-delta dq-peak-delta--save", children: t("peak.saving", { amount: fmt(shiftSaving) }) })
1733
- ] })
1838
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-peak-figures", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1839
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: t("peak.shift") }),
1840
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat-value", children: [
1841
+ "\xA5 ",
1842
+ fmt(split.offPeakEraCost),
1843
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-peak-delta dq-peak-delta--save", children: t("peak.saving", { amount: fmt(shiftSaving) }) })
1734
1844
  ] })
1735
- ] }),
1845
+ ] }) }),
1736
1846
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { className: "dq-peak-foot", children: [
1737
1847
  t("peak.schedule", { windows: pricing.peakWindows.join(t("common.listSeparator")) }),
1738
- pricing.splitActive ? t("peak.activeHint") : t("peak.futureHint", { date: pricing.switchDate })
1848
+ t("peak.activeHint")
1739
1849
  ] })
1740
1850
  ] });
1741
1851
  }
@@ -1978,6 +2088,9 @@ function BalanceDashboard(props) {
1978
2088
  const [freshnessNow, setFreshnessNow] = (0, import_react4.useState)(Date.now());
1979
2089
  const hadDataRef = (0, import_react4.useRef)(cachedBalance !== null || cachedUsage !== null);
1980
2090
  const [widgetOn, setWidgetOn] = (0, import_react4.useState)(widgetVisibleStore.get());
2091
+ const [widgetTabIds, setWidgetTabIds] = (0, import_react4.useState)(widgetTabIdsStore.get());
2092
+ const [viewTabs, setViewTabs] = (0, import_react4.useState)(() => props.views.list());
2093
+ (0, import_react4.useEffect)(() => props.views.subscribe(() => setViewTabs(props.views.list())), [props.views]);
1981
2094
  const [lowBalance, setLowBalance] = (0, import_react4.useState)(lowBalanceStore.get());
1982
2095
  const [monthlyBudget, setMonthlyBudget] = (0, import_react4.useState)(monthlyBudgetStore.get());
1983
2096
  const [barMode, setBarMode] = (0, import_react4.useState)("daily");
@@ -1986,9 +2099,12 @@ function BalanceDashboard(props) {
1986
2099
  const [chartMetric, setChartMetric] = (0, import_react4.useState)(chartMetricStore.get());
1987
2100
  const [runwayHovering, setRunwayHovering] = (0, import_react4.useState)(false);
1988
2101
  const [runwayManuallyOpened, setRunwayManuallyOpened] = (0, import_react4.useState)(false);
2102
+ const [remainingHovering, setRemainingHovering] = (0, import_react4.useState)(false);
2103
+ const [remainingManuallyOpened, setRemainingManuallyOpened] = (0, import_react4.useState)(false);
1989
2104
  const budgetInputRef = (0, import_react4.useRef)(null);
1990
2105
  const rootRef = (0, import_react4.useRef)(null);
1991
2106
  const [composerHeight, setComposerHeight] = (0, import_react4.useState)(null);
2107
+ const [composerWidth, setComposerWidth] = (0, import_react4.useState)(null);
1992
2108
  const [sessionUsage, setSessionUsage] = (0, import_react4.useState)(null);
1993
2109
  const [loadingSessionUsage, setLoadingSessionUsage] = (0, import_react4.useState)(props.sessionId !== void 0 && props.sessionId !== "");
1994
2110
  const [sessionUsageError, setSessionUsageError] = (0, import_react4.useState)(null);
@@ -2000,16 +2116,22 @@ function BalanceDashboard(props) {
2000
2116
  const frame2 = getShellFrame(node);
2001
2117
  const composer2 = getComposerElement(frame2);
2002
2118
  const rect = composer2?.getBoundingClientRect() ?? null;
2003
- const next = rect !== null && rect.height > 0 ? Math.round(rect.height) : null;
2004
- setComposerHeight((prev) => prev === next ? prev : next);
2119
+ const nextHeight = rect !== null && rect.height > 0 ? Math.round(rect.height) : null;
2120
+ setComposerHeight((prev) => prev === nextHeight ? prev : nextHeight);
2121
+ const input2 = getComposerInputElement(frame2);
2122
+ const inputRect = input2?.getBoundingClientRect() ?? null;
2123
+ const nextWidth = inputRect !== null && inputRect.width > 0 ? Math.round(inputRect.width) : null;
2124
+ setComposerWidth((prev) => prev === nextWidth ? prev : nextWidth);
2005
2125
  };
2006
2126
  measure();
2007
2127
  const frame = getShellFrame(node);
2008
2128
  const composer = getComposerElement(frame);
2129
+ const input = getComposerInputElement(frame);
2009
2130
  let observer = null;
2010
2131
  if (typeof ResizeObserver === "function") {
2011
2132
  observer = new ResizeObserver(measure);
2012
2133
  if (composer !== null) observer.observe(composer);
2134
+ if (input !== null) observer.observe(input);
2013
2135
  if (frame !== null) observer.observe(frame);
2014
2136
  }
2015
2137
  window.addEventListener("resize", measure);
@@ -2018,6 +2140,12 @@ function BalanceDashboard(props) {
2018
2140
  window.removeEventListener("resize", measure);
2019
2141
  };
2020
2142
  }, []);
2143
+ (0, import_react4.useLayoutEffect)(() => {
2144
+ const node = rootRef.current;
2145
+ if (node === null) return;
2146
+ const scrollport = node.closest("[data-conversation-scroll]");
2147
+ if (scrollport instanceof HTMLElement) scrollport.scrollTop = 0;
2148
+ }, [props.sessionId]);
2021
2149
  (0, import_react4.useLayoutEffect)(() => {
2022
2150
  quotaViewActiveStore.set(true);
2023
2151
  return () => quotaViewActiveStore.set(false);
@@ -2118,6 +2246,13 @@ function BalanceDashboard(props) {
2118
2246
  setWidgetOn(next);
2119
2247
  widgetVisibleStore.set(next);
2120
2248
  };
2249
+ const widgetTabs = viewTabs.filter((tab) => tab.id !== "balance");
2250
+ const toggleWidgetTab = (id) => {
2251
+ const selected = widgetTabIds ?? widgetTabs.map((tab) => tab.id);
2252
+ const next = selected.includes(id) ? selected.filter((tabId) => tabId !== id) : [...selected, id];
2253
+ setWidgetTabIds(next);
2254
+ widgetTabIdsStore.set(next);
2255
+ };
2121
2256
  const changeLowBalance = (value) => {
2122
2257
  setLowBalance(value);
2123
2258
  lowBalanceStore.set(value);
@@ -2220,7 +2355,11 @@ function BalanceDashboard(props) {
2220
2355
  );
2221
2356
  const errorText = error === null ? null : localizeApiError(error, t, "error.query");
2222
2357
  const composerSafeAreaStyle = {
2223
- "--dq-composer-h": `${composerHeight ?? COMPOSER_FALLBACK_PX}px`
2358
+ "--dq-composer-h": `${composerHeight ?? COMPOSER_FALLBACK_PX}px`,
2359
+ // Only set once measured; .dq-balance's max-width falls back to 860px
2360
+ // without it (pre-measurement render keeps the settled wide-viewport
2361
+ // column, then narrows in place — no flash of the old width).
2362
+ ...composerWidth !== null ? { "--dq-composer-w": `${composerWidth}px` } : {}
2224
2363
  };
2225
2364
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-balance", ref: rootRef, style: composerSafeAreaStyle, children: [
2226
2365
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-status-row", children: [
@@ -2273,17 +2412,36 @@ function BalanceDashboard(props) {
2273
2412
  primary !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-balance-grid", children: [
2274
2413
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
2275
2414
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: t("balance.remaining") }),
2276
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("details", { className: "dq-stat-value dq-remaining", children: [
2277
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("summary", { title: t("balance.breakdownTitle"), children: [
2278
- fmt(primary.total),
2279
- " ",
2280
- primary.currency
2281
- ] }),
2282
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-remaining-breakdown", children: [
2283
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: t("balance.toppedUp", { amount: fmt(primary.toppedUp) }) }),
2284
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-remaining-granted", children: t("balance.granted", { amount: fmt(primary.granted) }) })
2285
- ] })
2286
- ] })
2415
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2416
+ "details",
2417
+ {
2418
+ className: "dq-stat-value dq-remaining",
2419
+ open: remainingHovering || remainingManuallyOpened,
2420
+ onPointerEnter: () => setRemainingHovering(true),
2421
+ onPointerLeave: () => setRemainingHovering(false),
2422
+ children: [
2423
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2424
+ "summary",
2425
+ {
2426
+ title: t("balance.breakdownTitle"),
2427
+ onClick: (e) => {
2428
+ e.preventDefault();
2429
+ setRemainingManuallyOpened((v) => !v);
2430
+ },
2431
+ children: [
2432
+ fmt(primary.total),
2433
+ " ",
2434
+ primary.currency
2435
+ ]
2436
+ }
2437
+ ),
2438
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-remaining-breakdown", children: [
2439
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: t("balance.toppedUp", { amount: fmt(primary.toppedUp) }) }),
2440
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-remaining-granted", children: t("balance.granted", { amount: fmt(primary.granted) }) })
2441
+ ] })
2442
+ ]
2443
+ }
2444
+ )
2287
2445
  ] }),
2288
2446
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
2289
2447
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: t("balance.runway") }),
@@ -2345,6 +2503,31 @@ function BalanceDashboard(props) {
2345
2503
  ),
2346
2504
  usage !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PricingNote, { pricing: usage.pricing })
2347
2505
  ] }),
2506
+ usage !== null && activeWindow !== null && activeWindow.vision !== void 0 && Array.isArray(activeWindow.visionDaily) && Array.isArray(activeWindow.visionSessions) ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card", children: [
2507
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-card-title", children: t("vision.title") }),
2508
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2509
+ VisionCard,
2510
+ {
2511
+ vision: activeWindow.vision,
2512
+ daily: activeWindow.visionDaily,
2513
+ sessions: activeWindow.visionSessions,
2514
+ windowLabel: activeWindowName,
2515
+ inputTokens: activeWindow.totals.input,
2516
+ costTotal: activeWindow.totals.cost
2517
+ }
2518
+ )
2519
+ ] }) : usage === null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card", children: [
2520
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-card-title", children: t("vision.title") }),
2521
+ loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-usage-totals", children: [64, 88, 72].map((w, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
2522
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w, h: 11 }),
2523
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 72, h: 20 })
2524
+ ] }, i)) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("vision.empty", { window: activeWindowName }) })
2525
+ ] }) : (
2526
+ // Host serving a pre-vision payload (e.g. not restarted yet): hide the
2527
+ // whole card instead of leaving a titled shell — it reappears as soon
2528
+ // as the host half serves the new shape.
2529
+ null
2530
+ ),
2348
2531
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card dq-card--primary", children: [
2349
2532
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card-head", children: [
2350
2533
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-card-title", children: t("usage.trendTitle") }),
@@ -2421,7 +2604,7 @@ function BalanceDashboard(props) {
2421
2604
  ] }),
2422
2605
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card", children: [
2423
2606
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-card-title", children: t("peak.title") }),
2424
- usage !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PeakCard, { split: usage.peakSplit, pricing: usage.pricing, currentCost: usage.totals.cost }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
2607
+ usage !== null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PeakCard, { split: usage.peakSplit, pricing: usage.pricing }) : loadingUsage ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
2425
2608
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-peak-track" }),
2426
2609
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-peak-figures", children: [
2427
2610
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 120, h: 11 }),
@@ -2465,6 +2648,23 @@ function BalanceDashboard(props) {
2465
2648
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: t("settings.widget") })
2466
2649
  ] }),
2467
2650
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-toggle-hint", children: t("settings.widgetHint") }),
2651
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting dq-setting--tabs", children: [
2652
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-setting-label", id: "dq-widget-tabs-label", children: t("settings.widgetTabs") }),
2653
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-control dq-widget-tabs", role: "group", "aria-labelledby": "dq-widget-tabs-label", children: [
2654
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-widget-tab-list", children: widgetTabs.map((tab) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "dq-widget-tab", children: [
2655
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2656
+ "input",
2657
+ {
2658
+ type: "checkbox",
2659
+ checked: widgetTabIds === null || widgetTabIds.includes(tab.id),
2660
+ onChange: () => toggleWidgetTab(tab.id)
2661
+ }
2662
+ ),
2663
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: tab.label })
2664
+ ] }, tab.id)) }),
2665
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-setting-hint", children: widgetTabs.length === 0 ? t("settings.widgetTabsEmpty") : t("settings.widgetTabsHint") })
2666
+ ] })
2667
+ ] }),
2468
2668
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting", children: [
2469
2669
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { className: "dq-setting-label", htmlFor: "dq-low-balance", children: t("settings.lowBalance") }),
2470
2670
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-control", children: [
@@ -2503,14 +2703,55 @@ function BalanceDashboard(props) {
2503
2703
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { id: "dq-monthly-budget-hint", className: "dq-setting-hint", children: monthlyBudget > 0 ? t("settings.monthlyBudgetOn", { amount: fmt(monthlyBudget) }) : t("settings.monthlyBudgetOff") })
2504
2704
  ] })
2505
2705
  ] })
2506
- ] })
2706
+ ] }),
2707
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-footer", children: "@cassius0924/dsh-usage-dashboard v0.6.0" })
2507
2708
  ] });
2508
2709
  }
2509
2710
 
2711
+ // src/client/message-cost.tsx
2712
+ var import_react5 = require("react");
2713
+ var import_jsx_runtime5 = require("react/jsx-runtime");
2714
+ var fmtTurnCost = (cost) => cost.toLocaleString(void 0, {
2715
+ minimumFractionDigits: cost > 0 && cost < 0.01 ? 4 : 2,
2716
+ maximumFractionDigits: cost > 0 && cost < 0.01 ? 6 : 4
2717
+ });
2718
+ var cachedData = (sessionId) => {
2719
+ const response = getCachedSessionUsage(sessionId);
2720
+ return response?.ok === true ? response.data ?? null : null;
2721
+ };
2722
+ function MessageCost({ sessionId, messageId }) {
2723
+ const { t } = useI18n();
2724
+ const [data, setData] = (0, import_react5.useState)(() => cachedData(sessionId));
2725
+ (0, import_react5.useEffect)(() => {
2726
+ setData(cachedData(sessionId));
2727
+ const unsubscribe = subscribeSessionUsage(sessionId, () => setData(cachedData(sessionId)));
2728
+ const hit = cachedData(sessionId)?.turns.some((turn2) => turn2.messageId === messageId) ?? false;
2729
+ if (!hit) void fetchSessionUsage(sessionId);
2730
+ return unsubscribe;
2731
+ }, [messageId, sessionId]);
2732
+ const turn = data?.turns.find((candidate) => candidate.messageId === messageId);
2733
+ if (turn === void 0) return null;
2734
+ const amount = `\xA5 ${fmtTurnCost(turn.cost)}`;
2735
+ const tooltip = t("messageCost.tooltip");
2736
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2737
+ "span",
2738
+ {
2739
+ className: "dq-message-cost",
2740
+ tabIndex: 0,
2741
+ "data-dq-tooltip": tooltip,
2742
+ "aria-label": t("messageCost.aria", { amount }),
2743
+ children: [
2744
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "dq-message-cost-separator", "aria-hidden": true, children: "\xB7" }),
2745
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "dq-message-cost-amount", children: amount })
2746
+ ]
2747
+ }
2748
+ );
2749
+ }
2750
+
2510
2751
  // src/client/styles.ts
2511
2752
  var css = `
2512
2753
  .dsh-quota-root{position:absolute;right:16px;bottom:16px;z-index:2147483000;pointer-events:auto;opacity:1;visibility:visible;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:13px;line-height:1.45;color:var(--dsw-alias-label-primary,#1f2328);transition:left .28s cubic-bezier(.22,1,.36,1),top .28s cubic-bezier(.22,1,.36,1),opacity .16s ease-out,visibility 0s linear}
2513
- .dsh-quota-root.dsh-quota-root--dashboard{opacity:0;visibility:hidden;pointer-events:none;transition:opacity .14s ease-out,visibility 0s linear .14s}
2754
+ .dsh-quota-root.dsh-quota-root--hidden{opacity:0;visibility:hidden;pointer-events:none;transition:opacity .14s ease-out,visibility 0s linear .14s}
2514
2755
  .dsh-quota-root.dsh-quota-dragging{transition:none}
2515
2756
  .dsh-quota-card,.dsh-quota-card *{box-sizing:border-box}
2516
2757
  .dsh-quota-card{background:var(--dsw-alias-bg-overlay,#ffffff);border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.12));border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,.18);padding:12px 14px;min-width:208px;max-width:270px;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);cursor:grab;touch-action:none;user-select:none;-webkit-user-select:none}
@@ -2540,12 +2781,28 @@ var css = `
2540
2781
  .dsh-quota-label{color:var(--dsw-alias-label-secondary,#59636e)}
2541
2782
  .dsh-quota-value{color:var(--dsw-alias-label-primary,#1f2328);font-variant-numeric:tabular-nums}
2542
2783
  .dsh-quota-error{color:var(--dsw-alias-state-error-primary,#cf222e)}
2784
+ .dq-message-cost{position:relative;display:inline-flex;align-items:center;order:10;gap:10px;height:28px;color:var(--dsw-alias-label-tertiary,#59636e);font-size:12px;line-height:20px;font-variant-numeric:tabular-nums;white-space:nowrap;cursor:help}
2785
+ .dq-message-cost-separator{cursor:default}
2786
+ .dq-message-cost-amount{text-decoration-line:underline;text-decoration-style:dotted;text-decoration-color:currentColor;text-underline-offset:4px}
2787
+ .dq-message-cost:hover{color:var(--dsw-alias-label-secondary,#3f4852)}
2788
+ .dq-message-cost:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px;border-radius:4px}
2789
+ .dq-message-cost::after{content:attr(data-dq-tooltip);position:absolute;left:0;top:calc(100% + 6px);z-index:2147483001;width:max-content;max-width:min(240px,calc(100vw - 24px));padding:6px 9px;border-radius:8px;background:var(--dsw-alias-bg-inverse,#1f2328);color:var(--dsw-alias-label-inverse,#fff);box-shadow:0 6px 18px rgba(0,0,0,.22);font-size:12px;font-weight:400;line-height:1.45;text-align:left;white-space:normal;pointer-events:none;opacity:0;visibility:hidden;transform:translateY(-3px);transition:opacity .12s ease-out,transform .12s cubic-bezier(.22,1,.36,1),visibility 0s linear .12s}
2790
+ .dq-message-cost:hover::after,.dq-message-cost:focus-visible::after{opacity:1;visibility:visible;transform:translateY(0);transition:opacity .12s ease-out,transform .12s cubic-bezier(.22,1,.36,1),visibility 0s linear}
2791
+ @media (hover:hover){
2792
+ [data-time-hover-root] .dq-message-cost{opacity:0;transition:opacity 80ms ease}
2793
+ [data-time-hover-root]:hover .dq-message-cost,[data-time-hover-root]:focus-within .dq-message-cost{opacity:1}
2794
+ }
2543
2795
  /* \u5361\u7247\u95F4\u8DDD\uFF08\u6B64\u5904 gap\uFF09\u523B\u610F\u5927\u4E8E\u5361\u5185\u5C0F\u8282\u95F4\u8DDD\uFF08margin-top:12/14/16px \u90A3\u4E00\u6863\uFF09\uFF0C\u5236\u9020"\u5361\u7247\u5916\u758F\u3001\u5361\u7247\u5185\u5BC6"\u7684
2544
2796
  \u758F\u5BC6\u5BF9\u6BD4\uFF1A24px \u76F8\u5BF9\u5361\u5185\u6700\u5927\u6863 16px \u4ECD\u6709 1.5 \u500D\u7EA7\u5DEE\uFF0C\u76F8\u5BF9\u5E38\u89C1\u6863 12px \u6709 2 \u500D\u7EA7\u5DEE\uFF0C\u4E0D\u4E0E .dq-card \u7684
2545
2797
  padding\uFF0816px/20px\uFF0C\u5361\u7247\u5185\u90E8\u547C\u5438\u611F\uFF0C\u8F6E\u6B21 36 \u5DF2\u5B9A\uFF09\u6DF7\u540C\u3002 */
2546
- .dq-balance{display:flex;flex-direction:column;gap:24px;padding:20px 24px 48px;max-width:860px;margin:0 auto;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary,#1f2328)}
2798
+ /* Width parity with the host composer: the dashboard column reserves the
2799
+ same max-width as the conversation reading column (860px, \u8F6E\u6B21 42 kept it
2800
+ deliberately), but on narrower viewports the fixed 860px would overhang
2801
+ the input box \u2014 measured in dashboard.tsx as --dq-composer-w, capped at
2802
+ the reading-column width so wide viewports are unchanged. */
2803
+ .dq-balance{display:flex;flex-direction:column;gap:24px;padding:20px 24px 48px;max-width:min(860px,var(--dq-composer-w,860px));margin:0 auto;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary,#1f2328)}
2547
2804
  @media (max-width:620px){.dq-balance{box-sizing:border-box;max-width:calc(100vw - 56px);padding:16px 16px calc(var(--dq-composer-h,126px) + 16px)}}
2548
- .dq-card{background:var(--dsw-alias-bg-layer-1,#ffffff);border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08));border-radius:12px;padding:16px}
2805
+ .dq-card{background:var(--dsw-alias-bg-layer-1,#ffffff);border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08));border-radius:12px;padding:16px;min-width:0;max-width:100%}
2549
2806
  .dq-card-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,#59636e);margin:0 0 12px;letter-spacing:.02em}
2550
2807
  .dq-card-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}
2551
2808
  .dq-card-head .dq-card-title{margin:0}
@@ -2606,12 +2863,15 @@ var css = `
2606
2863
  .dq-runway[open] .dq-runway-detail{display:block}
2607
2864
  /* hero number for the balance, matching .dq-period-cost; relies on cascade order (declared after
2608
2865
  .dq-stat-value, same specificity) to win over the .dq-stat-value it's paired with in JSX \u2014 keep it below */
2866
+ /* \u8F6E\u6B21 47: .dq-remaining is a controlled <details> too now (see dashboard.tsx), same reason as
2867
+ .dq-runway above \u2014 an unopened <details>'s non-summary children don't actually render in current
2868
+ Chromium regardless of author display value, so the old :hover{display:flex} branch was dead code. */
2609
2869
  .dq-remaining{position:relative;font-size:22px;font-weight:680;letter-spacing:-.01em;line-height:1.25}
2610
2870
  .dq-remaining>summary{display:inline-block;list-style:none;border-radius:4px;cursor:pointer;text-decoration:underline dotted var(--dsw-alias-border-l2,rgba(0,0,0,.25));text-underline-offset:4px}
2611
2871
  .dq-remaining>summary::-webkit-details-marker{display:none}
2612
2872
  .dq-remaining>summary:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:3px}
2613
2873
  .dq-remaining-breakdown{display:none;position:absolute;top:calc(100% + 6px);left:0;z-index:20;flex-direction:column;gap:4px;padding:8px 10px;background:var(--dsw-alias-bg-overlay,#ffffff);border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.12));border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.15);white-space:nowrap;font-size:12px;font-weight:500;color:var(--dsw-alias-label-primary,#1f2328)}
2614
- .dq-remaining:hover .dq-remaining-breakdown,.dq-remaining[open] .dq-remaining-breakdown{display:flex}
2874
+ .dq-remaining[open] .dq-remaining-breakdown{display:flex}
2615
2875
  .dq-remaining-granted{color:var(--dsw-alias-label-tertiary,#59636e);font-weight:400;font-size:11px}
2616
2876
  /* narrow screens: .dq-balance-grid wraps its 3 stats onto separate lines, so the absolutely-positioned
2617
2877
  .dq-remaining-breakdown/.dq-runway-detail popovers can float down over whichever stat now sits in the
@@ -2758,6 +3018,9 @@ p.dq-session-sub{margin:8px 0}
2758
3018
  .dq-setting-label{font-size:13px;color:var(--dsw-alias-label-primary,#1f2328);padding-top:6px;flex:none}
2759
3019
  .dq-setting-control{display:flex;align-items:center;gap:10px;flex-wrap:wrap;min-width:0}
2760
3020
  .dq-setting-hint{font-size:12px;color:var(--dsw-alias-label-secondary,#59636e);line-height:1.5}
3021
+ /* Page footer: the package name + version, injected at build time from
3022
+ package.json (see build.mjs) so it can never drift from the release. */
3023
+ .dq-footer{margin:18px 0 0;font-size:12px;text-align:center;color:var(--dsw-alias-label-tertiary,#59636e)}
2761
3024
  .dq-number{width:82px;padding:5px 8px;font-size:13px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.16));background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#1f2328);font-variant-numeric:tabular-nums}
2762
3025
  .dq-number:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:1px;border-color:transparent}
2763
3026
  .dq-links-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary,#1f2328);margin:0 0 8px}
@@ -2772,7 +3035,11 @@ p.dq-session-sub{margin:8px 0}
2772
3035
  .dq-toggle{display:flex;align-items:center;gap:10px;cursor:pointer;font-size:13px;margin-top:14px;padding-top:14px;border-top:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08))}
2773
3036
  .dq-toggle input{width:16px;height:16px;accent-color:var(--dsw-alias-state-business-primary,#0969da);cursor:pointer}
2774
3037
  .dq-toggle-hint{margin:5px 0 0 26px;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary,#59636e)}
2775
- .dq-toggle input:focus-visible,.dq-model-item input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px}
3038
+ .dq-widget-tabs{align-items:flex-start;flex-direction:column;gap:7px}
3039
+ .dq-widget-tab-list{display:flex;align-items:center;flex-wrap:wrap;gap:6px 14px}
3040
+ .dq-widget-tab{display:inline-flex;align-items:center;gap:7px;min-height:28px;color:var(--dsw-alias-label-primary,#1f2328);font-size:12px;cursor:pointer}
3041
+ .dq-widget-tab input{width:14px;height:14px;margin:0;accent-color:var(--dsw-alias-state-business-primary,#0969da);cursor:pointer;flex:none}
3042
+ .dq-toggle input:focus-visible,.dq-model-item input:focus-visible,.dq-widget-tab input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px}
2776
3043
  .dq-link:focus-visible,.dq-model-btn:focus-visible,.dq-chart-switch-btn:focus-visible,.dq-export-btn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px}
2777
3044
  .dq-usage-totals{display:flex;flex-wrap:wrap;gap:12px 24px;margin-bottom:16px}
2778
3045
  /* value itself (number+unit, e.g. "9999.9\u4E07") must never break across lines inside its own column \u2014
@@ -2780,6 +3047,16 @@ p.dq-session-sub{margin:8px 0}
2780
3047
  case is the font-size drop below, which keeps every measured stress value comfortably under the
2781
3048
  83px column width without ever hitting the ellipsis fallback (see round 38 PROGRESS entry) */
2782
3049
  .dq-usage-totals .dq-stat-value{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
3050
+ .dq-vision-share{display:inline-block;margin-left:6px;font-size:11px;font-weight:550;color:var(--dsw-alias-label-tertiary,#59636e)}
3051
+ .dq-vision-sessions{margin-top:12px;display:flex;flex-direction:column;gap:8px;min-width:0}
3052
+ /* The vision card's session rows reuse .dq-session-head, but \u2014 unlike the
3053
+ ranking card's rows \u2014 they sit directly under .dq-session with no
3054
+ .dq-session-summary-body wrapper to carry min-width:0. Without the chain
3055
+ below, a long session title (nowrap) sets the row's min-content width and
3056
+ can stretch the card past its siblings / the container. */
3057
+ .dq-vision-sessions .dq-session{min-width:0}
3058
+ .dq-vision-sessions .dq-session-head{min-width:0}
3059
+ .dq-vision-sessions .dq-session-title{flex:1 1 auto;min-width:0}
2783
3060
  @media (max-width:620px){.dq-usage-totals{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 24px}.dq-usage-totals .dq-stat{min-width:0}.dq-usage-totals .dq-stat-value{font-size:13px}}
2784
3061
  .dq-chart-peak{margin-left:10px;font-size:11px;font-weight:400;color:var(--dsw-alias-label-tertiary,#59636e);font-variant-numeric:tabular-nums}
2785
3062
  .dq-chart-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,#59636e);margin:16px 0 8px}
@@ -2882,14 +3159,15 @@ p.dq-session-sub{margin:8px 0}
2882
3159
  @media (prefers-reduced-motion:reduce){
2883
3160
  .dsh-quota-root,.dsh-quota-root *,
2884
3161
  .dsh-quota-root *::before,.dsh-quota-root *::after,
3162
+ .dq-message-cost,.dq-message-cost::after,
2885
3163
  .dq-balance,.dq-balance *,
2886
3164
  .dq-balance *::before,.dq-balance *::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}
2887
3165
  }
2888
3166
  `;
2889
3167
 
2890
3168
  // src/client/widget.tsx
2891
- var import_react5 = require("react");
2892
- var import_jsx_runtime5 = require("react/jsx-runtime");
3169
+ var import_react6 = require("react");
3170
+ var import_jsx_runtime6 = require("react/jsx-runtime");
2893
3171
  var MARGIN = 16;
2894
3172
  var CORNER_KEY = "widget.corner";
2895
3173
  var COLLAPSED_KEY = "widget.collapsed";
@@ -2937,30 +3215,68 @@ function cornerPos(corner, node, bounds) {
2937
3215
  if (corner === "bl") return { x: xLeft, y: yBottom };
2938
3216
  return { x: xRight, y: yBottom };
2939
3217
  }
2940
- function QuotaWidget() {
3218
+ function QuotaWidget(props) {
2941
3219
  const { t } = useI18n();
2942
- const [visible, setVisible] = (0, import_react5.useState)(widgetVisibleStore.get());
2943
- (0, import_react5.useEffect)(() => widgetVisibleStore.subscribe(() => setVisible(widgetVisibleStore.get())), []);
2944
- const [quotaViewActive2, setQuotaViewActive] = (0, import_react5.useState)(quotaViewActiveStore.get());
2945
- (0, import_react5.useEffect)(() => quotaViewActiveStore.subscribe(() => setQuotaViewActive(quotaViewActiveStore.get())), []);
2946
- const [lowBalance, setLowBalance] = (0, import_react5.useState)(lowBalanceStore.get());
2947
- (0, import_react5.useEffect)(() => lowBalanceStore.subscribe(() => setLowBalance(lowBalanceStore.get())), []);
3220
+ const sessionId = props.useSessions((state) => state.current);
3221
+ const [visible, setVisible] = (0, import_react6.useState)(widgetVisibleStore.get());
3222
+ (0, import_react6.useEffect)(() => widgetVisibleStore.subscribe(() => setVisible(widgetVisibleStore.get())), []);
3223
+ const [quotaViewActive2, setQuotaViewActive] = (0, import_react6.useState)(quotaViewActiveStore.get());
3224
+ (0, import_react6.useEffect)(() => quotaViewActiveStore.subscribe(() => setQuotaViewActive(quotaViewActiveStore.get())), []);
3225
+ const [lowBalance, setLowBalance] = (0, import_react6.useState)(lowBalanceStore.get());
3226
+ (0, import_react6.useEffect)(() => lowBalanceStore.subscribe(() => setLowBalance(lowBalanceStore.get())), []);
3227
+ const [selectedTabIds, setSelectedTabIds] = (0, import_react6.useState)(widgetTabIdsStore.get());
3228
+ (0, import_react6.useEffect)(() => widgetTabIdsStore.subscribe(() => setSelectedTabIds(widgetTabIdsStore.get())), []);
3229
+ const [viewTabs, setViewTabs] = (0, import_react6.useState)(() => props.views.list());
3230
+ (0, import_react6.useEffect)(() => props.views.subscribe(() => setViewTabs(props.views.list())), [props.views]);
3231
+ const [activeViewId, setActiveViewId] = (0, import_react6.useState)(null);
2948
3232
  const cachedBalance = getCachedBalance();
2949
- const [data, setData] = (0, import_react5.useState)(cachedBalance?.data ?? null);
2950
- const [error, setError] = (0, import_react5.useState)(null);
2951
- const [loading, setLoading] = (0, import_react5.useState)(cachedBalance === null);
2952
- const [collapsed, setCollapsed] = (0, import_react5.useState)(() => loadPref(COLLAPSED_KEY, isBoolean, false));
2953
- const [pos, setPos] = (0, import_react5.useState)(null);
2954
- const [corner, setCorner] = (0, import_react5.useState)(() => loadPref(CORNER_KEY, isCorner, "br"));
2955
- const [dragging, setDragging] = (0, import_react5.useState)(false);
2956
- const rootRef = (0, import_react5.useRef)(null);
2957
- const dragRef = (0, import_react5.useRef)(null);
3233
+ const [data, setData] = (0, import_react6.useState)(cachedBalance?.data ?? null);
3234
+ const [error, setError] = (0, import_react6.useState)(null);
3235
+ const [loading, setLoading] = (0, import_react6.useState)(cachedBalance === null);
3236
+ const [collapsed, setCollapsed] = (0, import_react6.useState)(() => loadPref(COLLAPSED_KEY, isBoolean, false));
3237
+ const [pos, setPos] = (0, import_react6.useState)(null);
3238
+ const [corner, setCorner] = (0, import_react6.useState)(() => loadPref(CORNER_KEY, isCorner, "br"));
3239
+ const [dragging, setDragging] = (0, import_react6.useState)(false);
3240
+ const rootRef = (0, import_react6.useRef)(null);
3241
+ const dragRef = (0, import_react6.useRef)(null);
2958
3242
  const readUsage = () => {
2959
3243
  const cached = getCachedUsage();
2960
3244
  return { data: cached?.ok === true ? cached.data ?? null : null, at: getCachedUsageAt() };
2961
3245
  };
2962
- const [usage, setUsage] = (0, import_react5.useState)(readUsage);
2963
- (0, import_react5.useEffect)(() => subscribeUsage(() => setUsage(readUsage())), []);
3246
+ const [usage, setUsage] = (0, import_react6.useState)(readUsage);
3247
+ (0, import_react6.useEffect)(() => subscribeUsage(() => setUsage(readUsage())), []);
3248
+ const readSessionUsage = (id) => {
3249
+ if (id === void 0) return null;
3250
+ const response = getCachedSessionUsage(id);
3251
+ return response?.ok === true ? response.data ?? null : null;
3252
+ };
3253
+ const [sessionUsage, setSessionUsage] = (0, import_react6.useState)(() => readSessionUsage(sessionId));
3254
+ (0, import_react6.useEffect)(() => {
3255
+ setSessionUsage(readSessionUsage(sessionId));
3256
+ if (sessionId === void 0) return;
3257
+ const unsubscribe = subscribeSessionUsage(sessionId, () => setSessionUsage(readSessionUsage(sessionId)));
3258
+ void fetchSessionUsage(sessionId);
3259
+ return unsubscribe;
3260
+ }, [sessionId]);
3261
+ (0, import_react6.useLayoutEffect)(() => {
3262
+ const viewIds = viewTabs.map((tab) => tab.id);
3263
+ const read = () => {
3264
+ const next = getActiveConversationViewId(getShellFrame(rootRef.current), viewIds);
3265
+ setActiveViewId((previous) => previous === next ? previous : next);
3266
+ };
3267
+ read();
3268
+ const frame = getShellFrame(rootRef.current);
3269
+ let observer = null;
3270
+ if (frame !== null && typeof MutationObserver === "function") {
3271
+ observer = new MutationObserver(read);
3272
+ observer.observe(frame, { subtree: true, childList: true, attributes: true, attributeFilter: ["aria-selected"] });
3273
+ }
3274
+ const poll = setInterval(read, 1e3);
3275
+ return () => {
3276
+ observer?.disconnect();
3277
+ clearInterval(poll);
3278
+ };
3279
+ }, [sessionId, viewTabs]);
2964
3280
  const load = async (showLoading, force = false) => {
2965
3281
  if (showLoading) setLoading(true);
2966
3282
  try {
@@ -2979,14 +3295,14 @@ function QuotaWidget() {
2979
3295
  setLoading(false);
2980
3296
  }
2981
3297
  };
2982
- (0, import_react5.useEffect)(() => {
3298
+ (0, import_react6.useEffect)(() => {
2983
3299
  void load(true);
2984
3300
  const id = setInterval(() => {
2985
3301
  void load(false);
2986
3302
  }, 6e4);
2987
3303
  return () => clearInterval(id);
2988
3304
  }, []);
2989
- (0, import_react5.useLayoutEffect)(() => {
3305
+ (0, import_react6.useLayoutEffect)(() => {
2990
3306
  if (!visible || quotaViewActive2) return;
2991
3307
  const node = rootRef.current;
2992
3308
  if (node === null) return;
@@ -3106,49 +3422,68 @@ function QuotaWidget() {
3106
3422
  endDrag(true);
3107
3423
  };
3108
3424
  if (!visible) return null;
3425
+ const allowedOnActiveView = activeViewId === null ? sessionId === void 0 : activeViewId !== "balance" && (selectedTabIds === null || selectedTabIds.includes(activeViewId));
3426
+ const hidden = quotaViewActive2 || !allowedOnActiveView;
3109
3427
  const primary = data !== null && data.balances.length > 0 ? data.balances[0] : null;
3110
3428
  const balanceValue = primary !== null ? Number(primary.total) : Number.NaN;
3111
3429
  const low = lowBalance > 0 && Number.isFinite(balanceValue) && balanceValue < lowBalance;
3112
3430
  const dotClass = primary === null ? "dsh-quota-dot dsh-quota-dot--idle" : data?.isAvailable === false ? "dsh-quota-dot dsh-quota-dot--error" : low ? "dsh-quota-dot dsh-quota-dot--warn" : "dsh-quota-dot";
3113
3431
  const ageMinutes = usage.at === null ? null : Math.floor((Date.now() - usage.at) / 6e4);
3114
3432
  const usageAgeText = ageMinutes === null || ageMinutes < 1 ? t("widget.updatedNow") : t("widget.stale", { minutes: ageMinutes });
3433
+ const sessionCostRow = sessionUsage === null ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-row", title: t("widget.sessionCostTitle"), children: [
3434
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dsh-quota-label", children: t("widget.sessionCost") }),
3435
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "dsh-quota-value", children: [
3436
+ "\xA5 ",
3437
+ fmtTurnCost(sessionUsage.cost)
3438
+ ] })
3439
+ ] });
3115
3440
  let body = null;
3116
3441
  if (!collapsed) {
3117
3442
  if (loading && data === null && error === null) {
3118
- body = /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "dsh-quota-body", children: t("widget.loading") });
3443
+ body = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-body", children: [
3444
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { children: t("widget.loading") }),
3445
+ sessionCostRow
3446
+ ] });
3119
3447
  } else if (error !== null && data === null) {
3120
- body = /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "dsh-quota-body dsh-quota-error", children: localizeApiError(error, t, "error.query") });
3448
+ body = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-body", children: [
3449
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "dsh-quota-error", children: localizeApiError(error, t, "error.query") }),
3450
+ sessionCostRow
3451
+ ] });
3121
3452
  } else if (primary !== null) {
3122
- body = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "dsh-quota-body", children: [
3123
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "dsh-quota-remaining-label", children: t("balance.remaining") }),
3124
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { title: low ? t("widget.lowTitle", { amount: fmt(lowBalance) }) : void 0, children: [
3125
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: `dsh-quota-total${low ? " dsh-quota-total--low" : ""}`, children: fmt(primary.total) }),
3126
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "dsh-quota-currency", children: primary.currency })
3453
+ body = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-body", children: [
3454
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "dsh-quota-remaining-label", children: t("balance.remaining") }),
3455
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { title: low ? t("widget.lowTitle", { amount: fmt(lowBalance) }) : void 0, children: [
3456
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: `dsh-quota-total${low ? " dsh-quota-total--low" : ""}`, children: fmt(primary.total) }),
3457
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dsh-quota-currency", children: primary.currency })
3127
3458
  ] }),
3128
- usage.data !== null && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "dsh-quota-row", title: t("widget.todayCostTitle", { age: usageAgeText }), children: [
3129
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "dsh-quota-label", children: t("widget.todayCost") }),
3130
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "dsh-quota-value", children: [
3459
+ usage.data !== null && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-row", title: t("widget.todayCostTitle", { age: usageAgeText }), children: [
3460
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dsh-quota-label", children: t("widget.todayCost") }),
3461
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "dsh-quota-value", children: [
3131
3462
  "\xA5 ",
3132
3463
  fmt(usage.data.summary.today.cost)
3133
3464
  ] })
3134
3465
  ] }),
3135
- data?.isAvailable === false && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "dsh-quota-row", children: [
3136
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "dsh-quota-label", children: t("common.status") }),
3137
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "dsh-quota-value dsh-quota-error", children: t("common.unavailable") })
3466
+ sessionCostRow,
3467
+ data?.isAvailable === false && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-row", children: [
3468
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dsh-quota-label", children: t("common.status") }),
3469
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dsh-quota-value dsh-quota-error", children: t("common.unavailable") })
3138
3470
  ] })
3139
3471
  ] });
3140
3472
  } else {
3141
- body = /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "dsh-quota-body dsh-quota-error", children: t("widget.noBalance") });
3473
+ body = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-body", children: [
3474
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "dsh-quota-error", children: t("widget.noBalance") }),
3475
+ sessionCostRow
3476
+ ] });
3142
3477
  }
3143
3478
  }
3144
3479
  const style = pos !== null ? { left: `${pos.x}px`, top: `${pos.y}px`, right: "auto", bottom: "auto" } : void 0;
3145
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3480
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3146
3481
  "div",
3147
3482
  {
3148
- className: `dsh-quota-root${dragging ? " dsh-quota-dragging" : ""}${quotaViewActive2 ? " dsh-quota-root--dashboard" : ""}`,
3483
+ className: `dsh-quota-root${dragging ? " dsh-quota-dragging" : ""}${hidden ? " dsh-quota-root--hidden" : ""}`,
3149
3484
  style,
3150
- "aria-hidden": quotaViewActive2,
3151
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
3485
+ "aria-hidden": hidden,
3486
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3152
3487
  "div",
3153
3488
  {
3154
3489
  ref: rootRef,
@@ -3160,14 +3495,14 @@ function QuotaWidget() {
3160
3495
  onPointerCancel: () => endDrag(false),
3161
3496
  onLostPointerCapture: () => endDrag(false),
3162
3497
  children: [
3163
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "dsh-quota-header", children: [
3164
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "dsh-quota-title", children: [
3165
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "dsh-quota-grip", title: t("widget.dragGrip"), children: "\u2807" }),
3166
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: dotClass }),
3167
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "dsh-quota-name", children: t("widget.title") })
3498
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-header", children: [
3499
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-title", children: [
3500
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dsh-quota-grip", title: t("widget.dragGrip"), children: "\u2807" }),
3501
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: dotClass }),
3502
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dsh-quota-name", children: t("widget.title") })
3168
3503
  ] }),
3169
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "dsh-quota-actions", children: [
3170
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3504
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-quota-actions", children: [
3505
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3171
3506
  "button",
3172
3507
  {
3173
3508
  className: "dsh-quota-btn",
@@ -3178,11 +3513,12 @@ function QuotaWidget() {
3178
3513
  onClick: () => {
3179
3514
  void load(true, true);
3180
3515
  void fetchUsage(true);
3516
+ if (sessionId !== void 0) void fetchSessionUsage(sessionId, true);
3181
3517
  },
3182
3518
  children: "\u21BB"
3183
3519
  }
3184
3520
  ),
3185
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3521
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3186
3522
  "button",
3187
3523
  {
3188
3524
  className: "dsh-quota-btn",
@@ -3200,7 +3536,7 @@ function QuotaWidget() {
3200
3536
  )
3201
3537
  ] })
3202
3538
  ] }),
3203
- collapsed && primary !== null && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "dsh-quota-collapsed-total-row", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: `dsh-quota-collapsed-total${low ? " dsh-quota-total--low" : ""}`, children: [
3539
+ collapsed && primary !== null && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "dsh-quota-collapsed-total-row", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: `dsh-quota-collapsed-total${low ? " dsh-quota-total--low" : ""}`, children: [
3204
3540
  fmt(primary.total),
3205
3541
  " ",
3206
3542
  primary.currency
@@ -3213,13 +3549,31 @@ function QuotaWidget() {
3213
3549
  );
3214
3550
  }
3215
3551
 
3552
+ // src/client/views.ts
3553
+ var labelOf = (entry) => {
3554
+ const { id = "", label } = entry.options;
3555
+ try {
3556
+ const resolved = typeof label === "function" ? label() : label;
3557
+ return typeof resolved === "string" && resolved !== "" ? resolved : id;
3558
+ } catch {
3559
+ return id;
3560
+ }
3561
+ };
3562
+ function conversationViewsSource(slots) {
3563
+ return {
3564
+ list: () => slots.entries("conversation.view").flatMap((entry) => entry.options.id === void 0 ? [] : [{ id: entry.options.id, label: labelOf(entry) }]),
3565
+ subscribe: (fn) => slots.subscribe("conversation.view", fn)
3566
+ };
3567
+ }
3568
+
3216
3569
  // src/client/index.tsx
3217
- var import_jsx_runtime6 = require("react/jsx-runtime");
3570
+ var import_jsx_runtime7 = require("react/jsx-runtime");
3218
3571
  var name = "dsh-usage-dashboard";
3219
3572
  var inject = ["slots", "locale"];
3220
3573
  function apply(ctx) {
3221
3574
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-usage-dashboard: browser dictionaries");
3222
3575
  const t = ctx.locale.bind(NS);
3576
+ const views = conversationViewsSource(ctx.slots);
3223
3577
  if (typeof document !== "undefined") {
3224
3578
  const tag = document.createElement("style");
3225
3579
  tag.dataset.plugin = "dsh-usage-dashboard";
@@ -3232,7 +3586,17 @@ function apply(ctx) {
3232
3586
  });
3233
3587
  ctx.slots.inject("shell.overlay", () => ctx.slots.register(
3234
3588
  { name: "shell.overlay", id: "deepseek-quota", order: 1e3, locale: NS, label: () => t("widget.title") },
3235
- ({ t: slotT }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(LocaleProvider, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ErrorBoundary, { t: slotT, silent: true, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(QuotaWidget, {}) }) })
3589
+ ({ t: slotT, useSessions }) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(LocaleProvider, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ErrorBoundary, { t: slotT, silent: true, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(QuotaWidget, { useSessions, views }) }) })
3590
+ ));
3591
+ ctx.slots.inject("conversation.chat.assistant-actions", () => ctx.slots.register(
3592
+ {
3593
+ name: "conversation.chat.assistant-actions",
3594
+ id: "dsh-usage-turn-cost",
3595
+ order: 5,
3596
+ locale: NS,
3597
+ label: () => t("messageCost.label")
3598
+ },
3599
+ ({ t: slotT, sessionId, messageId }) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(LocaleProvider, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ErrorBoundary, { t: slotT, silent: true, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(MessageCost, { sessionId, messageId }) }) })
3236
3600
  ));
3237
3601
  ctx.slots.inject("conversation.view", () => ctx.slots.register(
3238
3602
  { name: "conversation.view", id: "balance", order: 20, locale: NS, label: () => t("nav.quota") },
@@ -3243,7 +3607,7 @@ function apply(ctx) {
3243
3607
  // `sessionId` is typed optional defensively even though the host is
3244
3608
  // expected to always supply it: BalanceDashboard renders the current-
3245
3609
  // session card only when it actually has one, never throws either way.
3246
- ({ t: slotT, sessionId }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(LocaleProvider, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ErrorBoundary, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BalanceDashboard, { sessionId }) }) })
3610
+ ({ t: slotT, sessionId }) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(LocaleProvider, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ErrorBoundary, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(BalanceDashboard, { sessionId, views }) }) })
3247
3611
  ));
3248
3612
  }
3249
3613
  return module.exports; } });