@cassius0924/dsh-usage-dashboard 0.5.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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![license](https://img.shields.io/npm/l/@cassius0924/dsh-usage-dashboard?color=blue)](./LICENSE)
5
5
 
6
6
  在 [DSH](https://github.com/deepseek-ai)(DeepSeek Harness)的 Web GUI 里,随时看得见 DeepSeek 的钱花在哪:
7
- **余额还能撑几天、今天花了多少、哪个模型最贵、缓存替你省了多少,以及 2026-08-17 峰谷定价之后账单会变成什么样。**
7
+ **余额还能撑几天、今天花了多少、哪个模型最贵、缓存替你省了多少、钱里有多少来自图片**,以及峰谷定价生效后把任务挪到闲时能省多少。
8
8
 
9
9
  ![dsh-usage-dashboard](docs/hero.png)
10
10
 
@@ -23,7 +23,8 @@
23
23
  | 今天花了多少?这个月呢? | 只有「累计」,没有时间感 |
24
24
  | 钱花在哪个模型上? | 不知道 |
25
25
  | 缓存到底帮我省了多少? | 完全看不到 |
26
- | 8-17 涨价后我的账单会变成多少? | 只能自己算 |
26
+ | 钱里有多少来自图片? | 多模态模型上线后更是看不见 |
27
+ | 高峰时段多付了多少? | 只能自己算 |
27
28
 
28
29
  这个插件把这些问题逐个翻译成一眼能看懂的数字。**费用全部为估算**,算法与单价公开可查(见[费用是怎么算的](#费用是怎么算的))。
29
30
 
@@ -59,13 +60,13 @@
59
60
 
60
61
  展开**计价说明**能看到费用估算实际套用的单价表——估算不该是个黑箱。
61
62
 
62
- ### 涨价之后会变成多少
63
+ ### 高峰时段多付了多少
63
64
 
64
65
  <img src="docs/peak.png" width="860" alt="高峰 / 闲时分布">
65
66
 
66
67
  DeepSeek 从 2026-08-17 00:00 起改峰谷定价,高峰(北京时间 09:00–12:00、14:00–18:00)价格是闲时的两倍。
67
68
 
68
- 这张卡告诉你用量落在峰谷两侧的比例,并且**在新价生效前就把账单重算一遍**:上图这份用量现价 ¥14.64,新价下是 ¥45.84(+213%)。生效之后,这里会换成「把高峰用量挪到闲时能省多少」。
69
+ 这张卡告诉你用量落在峰谷两侧的比例,以及**如果高峰用量都挪到闲时能省多少**——批量、可延后的任务放到闲时跑,同样的 token 只要一半的钱。
69
70
 
70
71
  ### 缓存替你省了多少
71
72
 
@@ -75,6 +76,16 @@ DeepSeek 从 2026-08-17 00:00 起改峰谷定价,高峰(北京时间 09:00
75
76
 
76
77
  命中率掉到 60% 以下时,文案会换成怎么把它救回来的建议。
77
78
 
79
+ ### 钱里有多少来自图片
80
+
81
+ > 截图待补拍:当前机器没有真实图片用量时,卡片显示的是空态,以下先以文字说明功能。
82
+
83
+ `deepseek-v4-flash-vision-exp` 上线后,图片按尺寸折算成 token 与文本一并计费,但「看了一堆截图」和「纯文本」在账单里长得一模一样。
84
+
85
+ 这张卡重放会话日志时顺手数出图片:用户消息里的附件和 tool result 里的截图都会统计(不用读附件本体,日志里就有宽高)。每组数字给出**图片张数、按官方换算规则估算的 token 与费用,以及占窗口输入 / 费用的比例**,旁边是每天图片数的趋势和「图片最多的会话」排行——一个不停刷截图的循环会话,在这里一眼就能认出来。
86
+
87
+ 图片归属到随后一次模型调用,按该调用的时段与模型单价估算;实际 token 数以接口返回的用量为准。
88
+
78
89
  ### 钱花在哪个模型上
79
90
 
80
91
  <img src="docs/models.png" width="860" alt="模型成本排行">
@@ -129,23 +140,30 @@ dsh plugin --profile web add ./path/to/dsh-usage-dashboard
129
140
 
130
141
  装好后打开 GUI,**进入任意一个会话**(「额度」tab 挂在 `conversation.view` 上,New Session 首页没有 tab 栏),顶部就能看到 `Chat / Trajectory / 额度`。
131
142
 
143
+ > 兼容性:同时支持两代宿主接口——旧版 `readFrom(id, 0)` 与新版 handle 化 `open(id, 'read')` / `read` / `close`。
144
+ > 宿主升级到移除 `readFrom` 的版本后,插件无需更换版本,会话日志照常重放。
145
+
132
146
  ## 费用是怎么算的
133
147
 
134
148
  费用是**估算**,不是账单。规则都在 [`src/pricing.ts`](src/pricing.ts) 一个文件里:
135
149
 
136
- - 逐条用量记录按「**模型** + **是否落在高峰时段**」计价,而不是全局一套价。
137
- - 2026-08-17 00:00(北京时间)之前的记录按旧的固定价;之后按峰谷价,闲时为高峰的一半。
150
+ - 逐条用量记录按「**模型** + **是否落在高峰时段** + **当时的价目表**」计价,而不是全局一套价。
151
+ - 2026-08-17 00:00(北京时间)之前按旧的固定价;之后按峰谷价,闲时为高峰的一半。
152
+ - 2026-09-10 12:00 起 Flash 系列降价(V4.1-Flash),高峰时段同时收紧为**周一至周五**。
153
+ - 2026-09-14 12:00 起 `deepseek-v4-pro` 的请求由 V4.1-Flash 承接,按 Flash 价计费。
138
154
  - 高峰时段按**北京时间**判定,不随机器时区漂移。
139
155
  - 未识别的模型按 `deepseek-v4-pro`(较贵的一侧)计价。
140
156
 
141
- 单价(CNY / 百万 tokens):
157
+ 单价(CNY / 百万 tokens,与官方价目表的两档一致):
142
158
 
143
159
  | 模型 | 时段 | 输入·缓存命中 | 输入·未命中 | 输出 |
144
160
  |---|---|---|---|---|
145
- | deepseek-v4-pro | 08-17 前固定 | 0.025 | 3 | 6 |
161
+ | deepseek-flash | 高峰 / 闲时 | 0.04 / 0.02 | 2 / 1 | 8 / 4 |
146
162
  | deepseek-v4-pro | 高峰 / 闲时 | 0.3 / 0.15 | 9 / 4.5 | 27 / 13.5 |
147
- | deepseek-v4-flash | 08-17 前固定 | 0.02 | 1 | 2 |
148
- | deepseek-v4-flash | 高峰 / 闲时 | 0.1 / 0.05 | 3 / 1.5 | 9 / 4.5 |
163
+
164
+ `deepseek-v4-flash`、`deepseek-v4-flash-vision-exp` 是旧模型名,现由 `deepseek-flash` 承接、按 Flash 价计费;2026-09-14 12:00 起 `deepseek-v4-pro` 的请求也路由到 V4.1-Flash,按 Flash 价计费。历史各时期单价:08-17 前 pro 0.025/3/6、flash 0.02/1/2;08-17 – 09-10 12:00 的 flash 高峰 0.1/3/9、闲时 0.05/1.5/4.5。
165
+
166
+ 图片不另外收费,而是按尺寸折算成 token 与文本一并计费:官方规则是先把图片按比例缩放到 ~800×800(小于 ~384×384 的放大),token 与缩放后面积成正比、每张上限 384 tokens。多模态卡的估算就套这条规则。
149
167
 
150
168
  DeepSeek 再调价时,只改这张表。
151
169
 
@@ -192,6 +210,7 @@ src/
192
210
 
193
211
  - 用量数据来自**本机** DSH 会话日志,不含其它机器/账户的用量。
194
212
  - 费用是估算:不含 DeepSeek 侧的折扣、赠金消耗顺序等因素,以官方账单为准。
213
+ - 图片 token 按官方换算规则估算,且把图片归属到随后一次模型调用;若缺少图片宽高或图片后没有成功的调用,则不纳入估算(无调用即无计费)。
195
214
  - 首次加载用量需要重放全部会话日志(本机实测约 5 秒),因此有 5 分钟的服务端记忆化;界面在此期间显示骨架屏。
196
215
 
197
216
  ## License
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
  }
@@ -332,16 +332,15 @@ var zh = {
332
332
  "pricing.currentOffPeak": "\u5F53\u524D \u95F2\u65F6",
333
333
  "pricing.model": "\u6A21\u578B",
334
334
  "pricing.period": "\u65F6\u6BB5",
335
- "pricing.unitPrice": "\u5355\u4EF7",
336
335
  "pricing.inputCacheHit": "\u8F93\u5165\xB7\u7F13\u5B58\u547D\u4E2D",
337
336
  "pricing.inputCacheMiss": "\u8F93\u5165\xB7\u672A\u547D\u4E2D",
338
337
  "pricing.output": "\u8F93\u51FA",
339
338
  "pricing.peak": "\u9AD8\u5CF0",
340
- "pricing.fixed": "\u56FA\u5B9A",
341
339
  "pricing.offPeak": "\u95F2\u65F6",
342
340
  "pricing.unit": "\u5355\u4F4D\uFF1A{currency} / \u767E\u4E07 tokens\u3002",
343
- "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",
344
- "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",
345
344
  "pricing.unknown": "\u672A\u77E5\u6A21\u578B\u6309 deepseek-v4-pro \u8BA1\u4EF7\u3002",
346
345
  "coverage.noRange": "\u6682\u65E0\u6709\u6548\u7528\u91CF\u65F6\u95F4",
347
346
  "coverage.title": "\u7EDF\u8BA1\u8303\u56F4",
@@ -375,13 +374,23 @@ var zh = {
375
374
  "peak.aria": "\u9AD8\u5CF0\u65F6\u6BB5 {peak}%\uFF0C\u95F2\u65F6 {offPeak}%",
376
375
  "peak.peakLegend": "\u9AD8\u5CF0 {share}% \xB7 {tokens} tokens \xB7 {calls}",
377
376
  "peak.offPeakLegend": "\u95F2\u65F6 {share}% \xB7 {tokens} tokens \xB7 {calls}",
378
- "peak.newPrice": "\u540C\u6837\u7528\u91CF\u5728\u65B0\u4EF7\u4E0B",
379
- "peak.increase": "\u8F83\u73B0\u4EF7 +{percent}%",
380
377
  "peak.shift": "\u82E5\u9AD8\u5CF0\u7528\u91CF\u90FD\u632A\u5230\u95F2\u65F6",
381
378
  "peak.saving": "\u53EF\u7701 \xA5{amount}",
382
- "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",
383
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",
384
- "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}",
385
394
  "sessions.empty": "\u8FD8\u6CA1\u6709\u4EA7\u751F\u8D39\u7528\u7684\u4F1A\u8BDD\u3002",
386
395
  "sessions.detail": "{tokens} tokens \xB7 {calls} \xB7 {ago}",
387
396
  "sessions.share": "\u5360 {percent}%",
@@ -581,16 +590,15 @@ var en = {
581
590
  "pricing.currentOffPeak": "Off-peak now",
582
591
  "pricing.model": "Model",
583
592
  "pricing.period": "Period",
584
- "pricing.unitPrice": "Rate",
585
593
  "pricing.inputCacheHit": "Input \xB7 cache hit",
586
594
  "pricing.inputCacheMiss": "Input \xB7 cache miss",
587
595
  "pricing.output": "Output",
588
596
  "pricing.peak": "Peak",
589
- "pricing.fixed": "Fixed",
590
597
  "pricing.offPeak": "Off-peak",
591
598
  "pricing.unit": "Unit: {currency} / million tokens. ",
592
- "pricing.splitNote": "Peak periods are {windows} Beijing time; off-peak rates are half price. Usage before {date} is still estimated at the old rates. ",
593
- "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. ",
594
602
  "pricing.unknown": "Unknown models use deepseek-v4-pro rates.",
595
603
  "coverage.noRange": "No valid usage timestamps",
596
604
  "coverage.title": "Coverage",
@@ -624,13 +632,23 @@ var en = {
624
632
  "peak.aria": "{peak}% peak, {offPeak}% off-peak",
625
633
  "peak.peakLegend": "Peak {share}% \xB7 {tokens} tokens \xB7 {calls}",
626
634
  "peak.offPeakLegend": "Off-peak {share}% \xB7 {tokens} tokens \xB7 {calls}",
627
- "peak.newPrice": "Same usage at new rates",
628
- "peak.increase": "+{percent}% vs current rates",
629
635
  "peak.shift": "If all peak usage moved off-peak",
630
636
  "peak.saving": "Save \xA5{amount}",
631
- "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. ",
632
638
  "peak.activeHint": "Run batch or delay-tolerant work off-peak to pay half as much for the same tokens.",
633
- "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}",
634
652
  "sessions.empty": "No sessions have incurred cost yet.",
635
653
  "sessions.detail": "{tokens} tokens \xB7 {calls} \xB7 {ago}",
636
654
  "sessions.share": "{percent}% of total",
@@ -1137,9 +1155,25 @@ function getShellFrame(node) {
1137
1155
  return overlay?.parentElement ?? null;
1138
1156
  }
1139
1157
  function getComposerElement(frame) {
1140
- if (frame === null) return null;
1158
+ if (frame === null) {
1159
+ return slotElement(document.documentElement, "conversation.composer.dock", "ancestor");
1160
+ }
1141
1161
  return slotElement(frame, "conversation.composer.dock", "ancestor");
1142
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
+ }
1143
1177
  function getActiveConversationViewId(frame, viewIds) {
1144
1178
  if (viewIds.length === 0) return null;
1145
1179
  const header = frame === null ? null : slotElement(frame, "conversation.session.header", "child");
@@ -1443,30 +1477,29 @@ var rate = (value) => `\xA5${value.toLocaleString(void 0, { maximumFractionDigit
1443
1477
  function PricingNote(props) {
1444
1478
  const { t } = useI18n();
1445
1479
  const { pricing } = props;
1446
- const split = pricing.splitActive;
1447
1480
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("details", { className: "dq-pricing", children: [
1448
1481
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("summary", { className: "dq-pricing-summary", children: [
1449
1482
  t("pricing.title"),
1450
- 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") })
1451
1484
  ] }),
1452
1485
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-pricing-body", children: [
1453
1486
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("table", { className: "dq-pricing-table", children: [
1454
1487
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
1455
1488
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: t("pricing.model") }),
1456
- /* @__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") }),
1457
1490
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: t("pricing.inputCacheHit") }),
1458
1491
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: t("pricing.inputCacheMiss") }),
1459
1492
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: t("pricing.output") })
1460
1493
  ] }) }),
1461
1494
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("tbody", { children: pricing.tiers.map((tier) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react4.Fragment, { children: [
1462
1495
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
1463
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { rowSpan: tier.offPeak !== null ? 2 : 1, children: tier.model }),
1464
- /* @__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") }),
1465
1498
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.peak.cacheHit) }),
1466
1499
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.peak.input) }),
1467
1500
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.peak.output) })
1468
1501
  ] }),
1469
- tier.offPeak !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
1502
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
1470
1503
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: t("pricing.offPeak") }),
1471
1504
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.offPeak.cacheHit) }),
1472
1505
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.offPeak.input) }),
@@ -1476,7 +1509,9 @@ function PricingNote(props) {
1476
1509
  ] }),
1477
1510
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { className: "dq-pricing-foot", children: [
1478
1511
  t("pricing.unit", { currency: pricing.currency }),
1479
- 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"),
1480
1515
  t("pricing.unknown")
1481
1516
  ] })
1482
1517
  ] })
@@ -1728,15 +1763,50 @@ function CacheCard(props) {
1728
1763
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-cache-foot", children: low ? t("cache.lowHint") : t("cache.goodHint") })
1729
1764
  ] });
1730
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
+ }
1731
1803
  function PeakCard(props) {
1732
1804
  const { t, locale } = useI18n();
1733
- const { split, pricing, currentCost } = props;
1805
+ const { split, pricing } = props;
1734
1806
  const total = split.peak.total + split.offPeak.total;
1735
1807
  if (total === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("peak.empty") });
1736
1808
  const peakShare = split.peak.total / total;
1737
1809
  const shiftSaving = split.peakEraCost - split.offPeakEraCost;
1738
- const increase = split.peakEraCost - currentCost;
1739
- const increasePercent = currentCost > 0 ? increase / currentCost * 100 : 0;
1740
1810
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-peak", children: [
1741
1811
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1742
1812
  "div",
@@ -1765,27 +1835,17 @@ function PeakCard(props) {
1765
1835
  })
1766
1836
  ] })
1767
1837
  ] }),
1768
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-peak-figures", children: [
1769
- !pricing.splitActive && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1770
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: t("peak.newPrice") }),
1771
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat-value", children: [
1772
- "\xA5 ",
1773
- fmt(split.peakEraCost),
1774
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-peak-delta", children: t("peak.increase", { percent: increasePercent.toFixed(0) }) })
1775
- ] })
1776
- ] }),
1777
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1778
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: t("peak.shift") }),
1779
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat-value", children: [
1780
- "\xA5 ",
1781
- fmt(split.offPeakEraCost),
1782
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-peak-delta dq-peak-delta--save", children: t("peak.saving", { amount: fmt(shiftSaving) }) })
1783
- ] })
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) }) })
1784
1844
  ] })
1785
- ] }),
1845
+ ] }) }),
1786
1846
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { className: "dq-peak-foot", children: [
1787
1847
  t("peak.schedule", { windows: pricing.peakWindows.join(t("common.listSeparator")) }),
1788
- pricing.splitActive ? t("peak.activeHint") : t("peak.futureHint", { date: pricing.switchDate })
1848
+ t("peak.activeHint")
1789
1849
  ] })
1790
1850
  ] });
1791
1851
  }
@@ -2044,6 +2104,7 @@ function BalanceDashboard(props) {
2044
2104
  const budgetInputRef = (0, import_react4.useRef)(null);
2045
2105
  const rootRef = (0, import_react4.useRef)(null);
2046
2106
  const [composerHeight, setComposerHeight] = (0, import_react4.useState)(null);
2107
+ const [composerWidth, setComposerWidth] = (0, import_react4.useState)(null);
2047
2108
  const [sessionUsage, setSessionUsage] = (0, import_react4.useState)(null);
2048
2109
  const [loadingSessionUsage, setLoadingSessionUsage] = (0, import_react4.useState)(props.sessionId !== void 0 && props.sessionId !== "");
2049
2110
  const [sessionUsageError, setSessionUsageError] = (0, import_react4.useState)(null);
@@ -2055,16 +2116,22 @@ function BalanceDashboard(props) {
2055
2116
  const frame2 = getShellFrame(node);
2056
2117
  const composer2 = getComposerElement(frame2);
2057
2118
  const rect = composer2?.getBoundingClientRect() ?? null;
2058
- const next = rect !== null && rect.height > 0 ? Math.round(rect.height) : null;
2059
- 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);
2060
2125
  };
2061
2126
  measure();
2062
2127
  const frame = getShellFrame(node);
2063
2128
  const composer = getComposerElement(frame);
2129
+ const input = getComposerInputElement(frame);
2064
2130
  let observer = null;
2065
2131
  if (typeof ResizeObserver === "function") {
2066
2132
  observer = new ResizeObserver(measure);
2067
2133
  if (composer !== null) observer.observe(composer);
2134
+ if (input !== null) observer.observe(input);
2068
2135
  if (frame !== null) observer.observe(frame);
2069
2136
  }
2070
2137
  window.addEventListener("resize", measure);
@@ -2073,6 +2140,12 @@ function BalanceDashboard(props) {
2073
2140
  window.removeEventListener("resize", measure);
2074
2141
  };
2075
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]);
2076
2149
  (0, import_react4.useLayoutEffect)(() => {
2077
2150
  quotaViewActiveStore.set(true);
2078
2151
  return () => quotaViewActiveStore.set(false);
@@ -2282,7 +2355,11 @@ function BalanceDashboard(props) {
2282
2355
  );
2283
2356
  const errorText = error === null ? null : localizeApiError(error, t, "error.query");
2284
2357
  const composerSafeAreaStyle = {
2285
- "--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` } : {}
2286
2363
  };
2287
2364
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-balance", ref: rootRef, style: composerSafeAreaStyle, children: [
2288
2365
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-status-row", children: [
@@ -2426,6 +2503,31 @@ function BalanceDashboard(props) {
2426
2503
  ),
2427
2504
  usage !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PricingNote, { pricing: usage.pricing })
2428
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
+ ),
2429
2531
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card dq-card--primary", children: [
2430
2532
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card-head", children: [
2431
2533
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-card-title", children: t("usage.trendTitle") }),
@@ -2502,7 +2604,7 @@ function BalanceDashboard(props) {
2502
2604
  ] }),
2503
2605
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card", children: [
2504
2606
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-card-title", children: t("peak.title") }),
2505
- 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: [
2506
2608
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-peak-track" }),
2507
2609
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-peak-figures", children: [
2508
2610
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 120, h: 11 }),
@@ -2601,7 +2703,8 @@ function BalanceDashboard(props) {
2601
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") })
2602
2704
  ] })
2603
2705
  ] })
2604
- ] })
2706
+ ] }),
2707
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-footer", children: "@cassius0924/dsh-usage-dashboard v0.6.0" })
2605
2708
  ] });
2606
2709
  }
2607
2710
 
@@ -2692,9 +2795,14 @@ var css = `
2692
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
2693
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
2694
2797
  padding\uFF0816px/20px\uFF0C\u5361\u7247\u5185\u90E8\u547C\u5438\u611F\uFF0C\u8F6E\u6B21 36 \u5DF2\u5B9A\uFF09\u6DF7\u540C\u3002 */
2695
- .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)}
2696
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)}}
2697
- .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%}
2698
2806
  .dq-card-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,#59636e);margin:0 0 12px;letter-spacing:.02em}
2699
2807
  .dq-card-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}
2700
2808
  .dq-card-head .dq-card-title{margin:0}
@@ -2910,6 +3018,9 @@ p.dq-session-sub{margin:8px 0}
2910
3018
  .dq-setting-label{font-size:13px;color:var(--dsw-alias-label-primary,#1f2328);padding-top:6px;flex:none}
2911
3019
  .dq-setting-control{display:flex;align-items:center;gap:10px;flex-wrap:wrap;min-width:0}
2912
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)}
2913
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}
2914
3025
  .dq-number:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:1px;border-color:transparent}
2915
3026
  .dq-links-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary,#1f2328);margin:0 0 8px}
@@ -2936,6 +3047,16 @@ p.dq-session-sub{margin:8px 0}
2936
3047
  case is the font-size drop below, which keeps every measured stress value comfortably under the
2937
3048
  83px column width without ever hitting the ellipsis fallback (see round 38 PROGRESS entry) */
2938
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}
2939
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}}
2940
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}
2941
3062
  .dq-chart-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,#59636e);margin:16px 0 8px}