@cassius0924/dsh-usage-dashboard 0.5.0 → 0.6.1

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="模型成本排行">
@@ -124,28 +135,35 @@ dsh plugin --profile web add ./path/to/dsh-usage-dashboard
124
135
 
125
136
  前提:
126
137
 
127
- - 已配置 `DEEPSEEK_API_KEY`(「设置 → 模型」里填,或放在 `$DSH_HOME/.credentials.yaml`)——余额接口要用。
138
+ - 余额优先走 DSH 的**账号登录**(浏览器授权,不需要 API Key);未登录账号时才回退到 `DEEPSEEK_API_KEY`(「设置 → 模型」里填,或放在 `$DSH_HOME/.credentials.yaml`)。
128
139
  - 本机有 pnpm(`dsh plugin` 是 pnpm 的转发器)。
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}%",
@@ -458,6 +467,7 @@ var zh = {
458
467
  "error.credentialsUnavailable": "\u51ED\u8BC1\u670D\u52A1\u4E0D\u53EF\u7528",
459
468
  "error.credentialRead": "\u8BFB\u53D6 API Key \u5931\u8D25\uFF1A{detail}",
460
469
  "error.noApiKey": "\u672A\u914D\u7F6E DEEPSEEK_API_KEY\uFF08\u53EF\u5728\u300C\u8BBE\u7F6E \u2192 \u6A21\u578B\u300D\u4E2D\u586B\u5199\uFF09",
470
+ "error.accountBalanceUnavailable": "\u8D26\u53F7\u4F59\u989D\u63A5\u53E3\u4E0D\u53EF\u7528\uFF0C\u4E14\u672A\u914D\u7F6E DEEPSEEK_API_KEY",
461
471
  "error.balanceRequest": "\u8BF7\u6C42\u4F59\u989D\u63A5\u53E3\u5931\u8D25\uFF1A{detail}",
462
472
  "error.balanceHttp": "\u4F59\u989D\u63A5\u53E3\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP {status}\uFF09",
463
473
  "error.balanceParse": "\u89E3\u6790\u4F59\u989D\u54CD\u5E94\u5931\u8D25",
@@ -479,7 +489,7 @@ var zh = {
479
489
  "balance.overYear": "> 365 \u5929",
480
490
  "balance.noRunway": "\u8FD1 {days} \u5929\u6CA1\u6709\u7528\u91CF\uFF0C\u65E0\u6CD5\u4F30\u7B97",
481
491
  "balance.runwayTitle": "\u6309\u8FD1 {days} \u5929\u65E5\u5747 \xA5{cost} \u4F30\u7B97\uFF08\u542B\u65E0\u7528\u91CF\u7684\u65E5\u5B50\uFF1B\u4ECA\u5929\u5C1A\u672A\u8FC7\u5B8C\uFF09",
482
- "balance.empty": "\u8BFB\u4E0D\u5230\u4F59\u989D\u3002\u8BF7\u786E\u8BA4\u300C\u8BBE\u7F6E \u2192 \u6A21\u578B\u300D\u91CC\u5DF2\u586B DEEPSEEK_API_KEY\uFF0C\u7136\u540E\u70B9\u53F3\u4E0A\u89D2\u5237\u65B0\u3002",
492
+ "balance.empty": "\u8BFB\u4E0D\u5230\u4F59\u989D\u3002\u8BF7\u786E\u8BA4\u8D26\u53F7\u5DF2\u767B\u5F55\uFF0C\u6216\u5728\u300C\u8BBE\u7F6E \u2192 \u6A21\u578B\u300D\u4E2D\u586B\u5199 DEEPSEEK_API_KEY\uFF0C\u7136\u540E\u70B9\u53F3\u4E0A\u89D2\u5237\u65B0\u3002",
483
493
  "sessionUsage.title": "\u5F53\u524D\u4F1A\u8BDD\u6D88\u8017\uFF08\u8D39\u7528\u4E3A\u4F30\u7B97\uFF09",
484
494
  "sessionUsage.cost": "\u672C\u4F1A\u8BDD\u8D39\u7528",
485
495
  "sessionUsage.tokens": "\u672C\u4F1A\u8BDD tokens",
@@ -581,16 +591,15 @@ var en = {
581
591
  "pricing.currentOffPeak": "Off-peak now",
582
592
  "pricing.model": "Model",
583
593
  "pricing.period": "Period",
584
- "pricing.unitPrice": "Rate",
585
594
  "pricing.inputCacheHit": "Input \xB7 cache hit",
586
595
  "pricing.inputCacheMiss": "Input \xB7 cache miss",
587
596
  "pricing.output": "Output",
588
597
  "pricing.peak": "Peak",
589
- "pricing.fixed": "Fixed",
590
598
  "pricing.offPeak": "Off-peak",
591
599
  "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. ",
600
+ "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. ",
601
+ "pricing.visionNote": "Images are converted to tokens by size and charged together with text. ",
602
+ "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
603
  "pricing.unknown": "Unknown models use deepseek-v4-pro rates.",
595
604
  "coverage.noRange": "No valid usage timestamps",
596
605
  "coverage.title": "Coverage",
@@ -624,13 +633,23 @@ var en = {
624
633
  "peak.aria": "{peak}% peak, {offPeak}% off-peak",
625
634
  "peak.peakLegend": "Peak {share}% \xB7 {tokens} tokens \xB7 {calls}",
626
635
  "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
636
  "peak.shift": "If all peak usage moved off-peak",
630
637
  "peak.saving": "Save \xA5{amount}",
631
- "peak.schedule": "Peak periods are {windows} Beijing time; off-peak rates are half price. ",
638
+ "peak.schedule": "Peak periods are {windows} Beijing time, Monday to Friday; off-peak rates are half price. ",
632
639
  "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.",
640
+ "vision.empty": "No image input in {window}.",
641
+ "vision.title": "Image / multimodal usage",
642
+ "vision.images": "Images",
643
+ "vision.tokens": "Estimated image tokens",
644
+ "vision.cost": "Estimated cost",
645
+ "vision.tokensShare": "{percent}% of input",
646
+ "vision.costShare": "{percent}% of cost",
647
+ "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.",
648
+ "vision.sessionsTitle": "Sessions with the most images",
649
+ "vision.sessionRow": "{images} images \xB7 \xA5{cost}",
650
+ "vision.dailyTitle": "{window} \xB7 images per day",
651
+ "vision.barAria": "{date} \xB7 {images} images \xB7 est. {tokens} tokens",
652
+ "vision.barTitle": "{date} \xB7 {images} images \xB7 est. {tokens} tokens \xB7 \xA5{cost}",
634
653
  "sessions.empty": "No sessions have incurred cost yet.",
635
654
  "sessions.detail": "{tokens} tokens \xB7 {calls} \xB7 {ago}",
636
655
  "sessions.share": "{percent}% of total",
@@ -707,6 +726,7 @@ var en = {
707
726
  "error.credentialsUnavailable": "Credential service is unavailable",
708
727
  "error.credentialRead": "Could not read the API key: {detail}",
709
728
  "error.noApiKey": "DEEPSEEK_API_KEY is not configured (add it under Settings \u2192 Models)",
729
+ "error.accountBalanceUnavailable": "The account balance endpoint is unavailable and DEEPSEEK_API_KEY is not configured",
710
730
  "error.balanceRequest": "Balance request failed: {detail}",
711
731
  "error.balanceHttp": "Balance endpoint returned HTTP {status}",
712
732
  "error.balanceParse": "Could not parse the balance response",
@@ -728,7 +748,7 @@ var en = {
728
748
  "balance.overYear": "> 365 days",
729
749
  "balance.noRunway": "No usage in the last {days} days, so runway cannot be estimated",
730
750
  "balance.runwayTitle": "Based on \xA5{cost} average daily spend over the last {days} days (including idle days; today is incomplete)",
731
- "balance.empty": "Balance is unavailable. Add DEEPSEEK_API_KEY under Settings \u2192 Models, then refresh.",
751
+ "balance.empty": "Balance is unavailable. Sign in to your account or add DEEPSEEK_API_KEY under Settings \u2192 Models, then refresh.",
732
752
  "sessionUsage.title": "Current session spend (estimated)",
733
753
  "sessionUsage.cost": "This session",
734
754
  "sessionUsage.tokens": "Session tokens",
@@ -812,6 +832,7 @@ function localizeApiError(message, t, fallback) {
812
832
  if (message === "\u4F59\u989D\u52A0\u8F7D\u5931\u8D25") return t("error.balanceLoad");
813
833
  if (message === "\u7528\u91CF\u52A0\u8F7D\u5931\u8D25") return t("error.usageLoad");
814
834
  if (message === "\u672A\u914D\u7F6E DEEPSEEK_API_KEY\uFF08\u53EF\u5728\u300C\u8BBE\u7F6E \u2192 \u6A21\u578B\u300D\u4E2D\u586B\u5199\uFF09") return t("error.noApiKey");
835
+ if (message === "\u8D26\u53F7\u4F59\u989D\u63A5\u53E3\u4E0D\u53EF\u7528\uFF0C\u4E14\u672A\u914D\u7F6E DEEPSEEK_API_KEY") return t("error.accountBalanceUnavailable");
815
836
  if (message === "\u89E3\u6790\u4F59\u989D\u54CD\u5E94\u5931\u8D25") return t("error.balanceParse");
816
837
  if (message === "\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528") return t("error.persistenceUnavailable");
817
838
  if (message === "\u7F3A\u5C11\u4F1A\u8BDD id") return t("error.missingSessionId");
@@ -1137,9 +1158,25 @@ function getShellFrame(node) {
1137
1158
  return overlay?.parentElement ?? null;
1138
1159
  }
1139
1160
  function getComposerElement(frame) {
1140
- if (frame === null) return null;
1161
+ if (frame === null) {
1162
+ return slotElement(document.documentElement, "conversation.composer.dock", "ancestor");
1163
+ }
1141
1164
  return slotElement(frame, "conversation.composer.dock", "ancestor");
1142
1165
  }
1166
+ function getComposerInputElement(frame) {
1167
+ const root = frame ?? document.documentElement;
1168
+ const textarea = root.querySelector(
1169
+ '[data-slot="conversation.composer"] textarea, [data-slot="conversation.composer.bar"] textarea, textarea'
1170
+ );
1171
+ if (textarea === null) return null;
1172
+ let el = textarea.parentElement;
1173
+ for (let depth = 0; depth < 6 && el !== null; depth++) {
1174
+ const cs = getComputedStyle(el);
1175
+ if (cs.borderRadius !== "0px" && cs.backgroundColor !== "rgba(0, 0, 0, 0)") return el;
1176
+ el = el.parentElement;
1177
+ }
1178
+ return null;
1179
+ }
1143
1180
  function getActiveConversationViewId(frame, viewIds) {
1144
1181
  if (viewIds.length === 0) return null;
1145
1182
  const header = frame === null ? null : slotElement(frame, "conversation.session.header", "child");
@@ -1443,30 +1480,29 @@ var rate = (value) => `\xA5${value.toLocaleString(void 0, { maximumFractionDigit
1443
1480
  function PricingNote(props) {
1444
1481
  const { t } = useI18n();
1445
1482
  const { pricing } = props;
1446
- const split = pricing.splitActive;
1447
1483
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("details", { className: "dq-pricing", children: [
1448
1484
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("summary", { className: "dq-pricing-summary", children: [
1449
1485
  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") })
1486
+ /* @__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
1487
  ] }),
1452
1488
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-pricing-body", children: [
1453
1489
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("table", { className: "dq-pricing-table", children: [
1454
1490
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
1455
1491
  /* @__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") }),
1492
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: t("pricing.period") }),
1457
1493
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: t("pricing.inputCacheHit") }),
1458
1494
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: t("pricing.inputCacheMiss") }),
1459
1495
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: t("pricing.output") })
1460
1496
  ] }) }),
1461
1497
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("tbody", { children: pricing.tiers.map((tier) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react4.Fragment, { children: [
1462
1498
  /* @__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") }),
1499
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { rowSpan: 2, children: tier.model }),
1500
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: t("pricing.peak") }),
1465
1501
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.peak.cacheHit) }),
1466
1502
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.peak.input) }),
1467
1503
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.peak.output) })
1468
1504
  ] }),
1469
- tier.offPeak !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
1505
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
1470
1506
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: t("pricing.offPeak") }),
1471
1507
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.offPeak.cacheHit) }),
1472
1508
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: rate(tier.offPeak.input) }),
@@ -1476,7 +1512,9 @@ function PricingNote(props) {
1476
1512
  ] }),
1477
1513
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { className: "dq-pricing-foot", children: [
1478
1514
  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 }),
1515
+ t("pricing.splitNote", { windows: pricing.peakWindows.join(t("common.listSeparator")) }),
1516
+ t("pricing.visionNote"),
1517
+ t("pricing.modelNote"),
1480
1518
  t("pricing.unknown")
1481
1519
  ] })
1482
1520
  ] })
@@ -1728,15 +1766,50 @@ function CacheCard(props) {
1728
1766
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-cache-foot", children: low ? t("cache.lowHint") : t("cache.goodHint") })
1729
1767
  ] });
1730
1768
  }
1769
+ function VisionCard(props) {
1770
+ const { t, locale } = useI18n();
1771
+ const { vision, daily, sessions, windowLabel, inputTokens, costTotal } = props;
1772
+ if (vision.images === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("vision.empty", { window: windowLabel }) });
1773
+ const tokensShare = inputTokens > 0 ? vision.imageTokens / inputTokens * 100 : 0;
1774
+ const costShare = costTotal > 0 ? vision.cost / costTotal * 100 : 0;
1775
+ const labelEvery = daily.length <= 7 ? 1 : daily.length <= 30 ? 5 : daily.length <= 90 ? 15 : 30;
1776
+ const bars = daily.map((d) => ({
1777
+ label: d.date.slice(8, 10),
1778
+ value: d.images,
1779
+ title: t("vision.barTitle", { date: d.date, images: fmtInt(d.images), tokens: fmtCompact(d.imageTokens, locale), cost: fmt(d.cost) })
1780
+ }));
1781
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1782
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-usage-totals", children: [
1783
+ /* @__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) }) }),
1784
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("vision.tokens"), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat-value", children: [
1785
+ fmtCompact(vision.imageTokens, locale),
1786
+ tokensShare > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-vision-share", children: t("vision.tokensShare", { percent: tokensShare.toFixed(1) }) })
1787
+ ] }) }),
1788
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("vision.cost"), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat-value", children: [
1789
+ "\xA5 ",
1790
+ fmt(vision.cost),
1791
+ costShare > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-vision-share", children: t("vision.costShare", { percent: costShare.toFixed(1) }) })
1792
+ ] }) })
1793
+ ] }),
1794
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-chart-title", children: t("vision.dailyTitle", { window: windowLabel }) }),
1795
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Bars, { data: bars, height: 80, labelEvery }),
1796
+ sessions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-vision-sessions", children: [
1797
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-chart-title", children: t("vision.sessionsTitle") }),
1798
+ 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: [
1799
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-session-title", title: session.id, children: session.title }),
1800
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-session-cost", children: t("vision.sessionRow", { images: fmtInt(session.images), cost: fmt(session.cost) }) })
1801
+ ] }) }, session.id))
1802
+ ] }),
1803
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-cache-foot", children: t("vision.note") })
1804
+ ] });
1805
+ }
1731
1806
  function PeakCard(props) {
1732
1807
  const { t, locale } = useI18n();
1733
- const { split, pricing, currentCost } = props;
1808
+ const { split, pricing } = props;
1734
1809
  const total = split.peak.total + split.offPeak.total;
1735
1810
  if (total === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("peak.empty") });
1736
1811
  const peakShare = split.peak.total / total;
1737
1812
  const shiftSaving = split.peakEraCost - split.offPeakEraCost;
1738
- const increase = split.peakEraCost - currentCost;
1739
- const increasePercent = currentCost > 0 ? increase / currentCost * 100 : 0;
1740
1813
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-peak", children: [
1741
1814
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1742
1815
  "div",
@@ -1765,27 +1838,17 @@ function PeakCard(props) {
1765
1838
  })
1766
1839
  ] })
1767
1840
  ] }),
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
- ] })
1841
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-peak-figures", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
1842
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: t("peak.shift") }),
1843
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat-value", children: [
1844
+ "\xA5 ",
1845
+ fmt(split.offPeakEraCost),
1846
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-peak-delta dq-peak-delta--save", children: t("peak.saving", { amount: fmt(shiftSaving) }) })
1784
1847
  ] })
1785
- ] }),
1848
+ ] }) }),
1786
1849
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { className: "dq-peak-foot", children: [
1787
1850
  t("peak.schedule", { windows: pricing.peakWindows.join(t("common.listSeparator")) }),
1788
- pricing.splitActive ? t("peak.activeHint") : t("peak.futureHint", { date: pricing.switchDate })
1851
+ t("peak.activeHint")
1789
1852
  ] })
1790
1853
  ] });
1791
1854
  }
@@ -2044,6 +2107,7 @@ function BalanceDashboard(props) {
2044
2107
  const budgetInputRef = (0, import_react4.useRef)(null);
2045
2108
  const rootRef = (0, import_react4.useRef)(null);
2046
2109
  const [composerHeight, setComposerHeight] = (0, import_react4.useState)(null);
2110
+ const [composerWidth, setComposerWidth] = (0, import_react4.useState)(null);
2047
2111
  const [sessionUsage, setSessionUsage] = (0, import_react4.useState)(null);
2048
2112
  const [loadingSessionUsage, setLoadingSessionUsage] = (0, import_react4.useState)(props.sessionId !== void 0 && props.sessionId !== "");
2049
2113
  const [sessionUsageError, setSessionUsageError] = (0, import_react4.useState)(null);
@@ -2055,16 +2119,22 @@ function BalanceDashboard(props) {
2055
2119
  const frame2 = getShellFrame(node);
2056
2120
  const composer2 = getComposerElement(frame2);
2057
2121
  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);
2122
+ const nextHeight = rect !== null && rect.height > 0 ? Math.round(rect.height) : null;
2123
+ setComposerHeight((prev) => prev === nextHeight ? prev : nextHeight);
2124
+ const input2 = getComposerInputElement(frame2);
2125
+ const inputRect = input2?.getBoundingClientRect() ?? null;
2126
+ const nextWidth = inputRect !== null && inputRect.width > 0 ? Math.round(inputRect.width) : null;
2127
+ setComposerWidth((prev) => prev === nextWidth ? prev : nextWidth);
2060
2128
  };
2061
2129
  measure();
2062
2130
  const frame = getShellFrame(node);
2063
2131
  const composer = getComposerElement(frame);
2132
+ const input = getComposerInputElement(frame);
2064
2133
  let observer = null;
2065
2134
  if (typeof ResizeObserver === "function") {
2066
2135
  observer = new ResizeObserver(measure);
2067
2136
  if (composer !== null) observer.observe(composer);
2137
+ if (input !== null) observer.observe(input);
2068
2138
  if (frame !== null) observer.observe(frame);
2069
2139
  }
2070
2140
  window.addEventListener("resize", measure);
@@ -2073,6 +2143,12 @@ function BalanceDashboard(props) {
2073
2143
  window.removeEventListener("resize", measure);
2074
2144
  };
2075
2145
  }, []);
2146
+ (0, import_react4.useLayoutEffect)(() => {
2147
+ const node = rootRef.current;
2148
+ if (node === null) return;
2149
+ const scrollport = node.closest("[data-conversation-scroll]");
2150
+ if (scrollport instanceof HTMLElement) scrollport.scrollTop = 0;
2151
+ }, [props.sessionId]);
2076
2152
  (0, import_react4.useLayoutEffect)(() => {
2077
2153
  quotaViewActiveStore.set(true);
2078
2154
  return () => quotaViewActiveStore.set(false);
@@ -2282,7 +2358,11 @@ function BalanceDashboard(props) {
2282
2358
  );
2283
2359
  const errorText = error === null ? null : localizeApiError(error, t, "error.query");
2284
2360
  const composerSafeAreaStyle = {
2285
- "--dq-composer-h": `${composerHeight ?? COMPOSER_FALLBACK_PX}px`
2361
+ "--dq-composer-h": `${composerHeight ?? COMPOSER_FALLBACK_PX}px`,
2362
+ // Only set once measured; .dq-balance's max-width falls back to 860px
2363
+ // without it (pre-measurement render keeps the settled wide-viewport
2364
+ // column, then narrows in place — no flash of the old width).
2365
+ ...composerWidth !== null ? { "--dq-composer-w": `${composerWidth}px` } : {}
2286
2366
  };
2287
2367
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-balance", ref: rootRef, style: composerSafeAreaStyle, children: [
2288
2368
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-status-row", children: [
@@ -2426,6 +2506,31 @@ function BalanceDashboard(props) {
2426
2506
  ),
2427
2507
  usage !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PricingNote, { pricing: usage.pricing })
2428
2508
  ] }),
2509
+ 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: [
2510
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-card-title", children: t("vision.title") }),
2511
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2512
+ VisionCard,
2513
+ {
2514
+ vision: activeWindow.vision,
2515
+ daily: activeWindow.visionDaily,
2516
+ sessions: activeWindow.visionSessions,
2517
+ windowLabel: activeWindowName,
2518
+ inputTokens: activeWindow.totals.input,
2519
+ costTotal: activeWindow.totals.cost
2520
+ }
2521
+ )
2522
+ ] }) : usage === null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card", children: [
2523
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-card-title", children: t("vision.title") }),
2524
+ 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: [
2525
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w, h: 11 }),
2526
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 72, h: 20 })
2527
+ ] }, i)) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("vision.empty", { window: activeWindowName }) })
2528
+ ] }) : (
2529
+ // Host serving a pre-vision payload (e.g. not restarted yet): hide the
2530
+ // whole card instead of leaving a titled shell — it reappears as soon
2531
+ // as the host half serves the new shape.
2532
+ null
2533
+ ),
2429
2534
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card dq-card--primary", children: [
2430
2535
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card-head", children: [
2431
2536
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-card-title", children: t("usage.trendTitle") }),
@@ -2502,7 +2607,7 @@ function BalanceDashboard(props) {
2502
2607
  ] }),
2503
2608
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card", children: [
2504
2609
  /* @__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: [
2610
+ 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
2611
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-peak-track" }),
2507
2612
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-peak-figures", children: [
2508
2613
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Skel, { w: 120, h: 11 }),
@@ -2601,7 +2706,8 @@ function BalanceDashboard(props) {
2601
2706
  /* @__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
2707
  ] })
2603
2708
  ] })
2604
- ] })
2709
+ ] }),
2710
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-footer", children: "@cassius0924/dsh-usage-dashboard v0.6.1" })
2605
2711
  ] });
2606
2712
  }
2607
2713
 
@@ -2692,9 +2798,14 @@ var css = `
2692
2798
  /* \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
2799
  \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
2800
  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)}
2801
+ /* Width parity with the host composer: the dashboard column reserves the
2802
+ same max-width as the conversation reading column (860px, \u8F6E\u6B21 42 kept it
2803
+ deliberately), but on narrower viewports the fixed 860px would overhang
2804
+ the input box \u2014 measured in dashboard.tsx as --dq-composer-w, capped at
2805
+ the reading-column width so wide viewports are unchanged. */
2806
+ .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
2807
  @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}
2808
+ .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
2809
  .dq-card-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,#59636e);margin:0 0 12px;letter-spacing:.02em}
2699
2810
  .dq-card-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}
2700
2811
  .dq-card-head .dq-card-title{margin:0}
@@ -2910,6 +3021,9 @@ p.dq-session-sub{margin:8px 0}
2910
3021
  .dq-setting-label{font-size:13px;color:var(--dsw-alias-label-primary,#1f2328);padding-top:6px;flex:none}
2911
3022
  .dq-setting-control{display:flex;align-items:center;gap:10px;flex-wrap:wrap;min-width:0}
2912
3023
  .dq-setting-hint{font-size:12px;color:var(--dsw-alias-label-secondary,#59636e);line-height:1.5}
3024
+ /* Page footer: the package name + version, injected at build time from
3025
+ package.json (see build.mjs) so it can never drift from the release. */
3026
+ .dq-footer{margin:18px 0 0;font-size:12px;text-align:center;color:var(--dsw-alias-label-tertiary,#59636e)}
2913
3027
  .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
3028
  .dq-number:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:1px;border-color:transparent}
2915
3029
  .dq-links-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary,#1f2328);margin:0 0 8px}
@@ -2936,6 +3050,16 @@ p.dq-session-sub{margin:8px 0}
2936
3050
  case is the font-size drop below, which keeps every measured stress value comfortably under the
2937
3051
  83px column width without ever hitting the ellipsis fallback (see round 38 PROGRESS entry) */
2938
3052
  .dq-usage-totals .dq-stat-value{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
3053
+ .dq-vision-share{display:inline-block;margin-left:6px;font-size:11px;font-weight:550;color:var(--dsw-alias-label-tertiary,#59636e)}
3054
+ .dq-vision-sessions{margin-top:12px;display:flex;flex-direction:column;gap:8px;min-width:0}
3055
+ /* The vision card's session rows reuse .dq-session-head, but \u2014 unlike the
3056
+ ranking card's rows \u2014 they sit directly under .dq-session with no
3057
+ .dq-session-summary-body wrapper to carry min-width:0. Without the chain
3058
+ below, a long session title (nowrap) sets the row's min-content width and
3059
+ can stretch the card past its siblings / the container. */
3060
+ .dq-vision-sessions .dq-session{min-width:0}
3061
+ .dq-vision-sessions .dq-session-head{min-width:0}
3062
+ .dq-vision-sessions .dq-session-title{flex:1 1 auto;min-width:0}
2939
3063
  @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
3064
  .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
3065
  .dq-chart-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,#59636e);margin:16px 0 8px}
@@ -3046,6 +3170,14 @@ p.dq-session-sub{margin:8px 0}
3046
3170
 
3047
3171
  // src/client/widget.tsx
3048
3172
  var import_react6 = require("react");
3173
+
3174
+ // src/client/sessions.ts
3175
+ function currentSessionIdOf(state) {
3176
+ const retained = Object.values(state.byId ?? {}).find((row) => (row?.retainedBy?.mainView ?? 0) > 0);
3177
+ return retained?.id ?? state.current;
3178
+ }
3179
+
3180
+ // src/client/widget.tsx
3049
3181
  var import_jsx_runtime6 = require("react/jsx-runtime");
3050
3182
  var MARGIN = 16;
3051
3183
  var CORNER_KEY = "widget.corner";
@@ -3096,7 +3228,7 @@ function cornerPos(corner, node, bounds) {
3096
3228
  }
3097
3229
  function QuotaWidget(props) {
3098
3230
  const { t } = useI18n();
3099
- const sessionId = props.useSessions((state) => state.current);
3231
+ const sessionId = props.useSessions(currentSessionIdOf);
3100
3232
  const [visible, setVisible] = (0, import_react6.useState)(widgetVisibleStore.get());
3101
3233
  (0, import_react6.useEffect)(() => widgetVisibleStore.subscribe(() => setVisible(widgetVisibleStore.get())), []);
3102
3234
  const [quotaViewActive2, setQuotaViewActive] = (0, import_react6.useState)(quotaViewActiveStore.get());