dsh-balance-widget 0.4.5 → 0.5.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.en.md CHANGED
@@ -9,11 +9,11 @@
9
9
  [![Node 24](https://img.shields.io/badge/Node%2024-ready-brightgreen?style=flat-square)](https://nodejs.org)
10
10
  [![Zero deps](https://img.shields.io/badge/dependencies-zero-brightgreen?style=flat-square)]()
11
11
 
12
- A balance & cost widget for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) Web GUI: a persistent sidebar footer card shows the account balance and a remaining-ratio bar; clicking opens a four-tier cost breakdown (last prompt / this session / today-this-project / today-all).
12
+ A balance & cost widget for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) Web GUI: a persistent sidebar footer card shows the account balance and today's cost; clicking opens a five-tier cost breakdown (balance / last prompt with its session name / today-this-session / today-this-workspace / today-all-workspaces).
13
13
 
14
14
  ## Preview
15
15
 
16
- | Sidebar card | Four-tier cost popover |
16
+ | Sidebar card | Five-tier cost popover |
17
17
  | --- | --- |
18
18
  | ![Sidebar card](docs/screenshot-corner.png) | ![Cost popover](docs/screenshot-popover.png) |
19
19
 
@@ -24,7 +24,7 @@ A balance & cost widget for the [DeepSeek Harness](https://github.com/deepseek-a
24
24
  | **Zero external dependencies** | ✅ Imports no `@deepseek-ai/*` packages, no native modules | ❌ Most depend on dsh SDK packages |
25
25
  | **Node 24 ready** | ✅ Works out of the box on any profile layout | ⚠️ Many community plugins still error on Node 24 |
26
26
  | **Boot stability** | ✅ Registers routes via official `ctx.webServer`; never conflicts with apiproxy | ⚠️ Some self-host HTTP servers that crash `dsh web` on boot |
27
- | **On-demand queries** | ✅ Fetches balance only on click; no polling, zero background requests | Some always-on badges refresh on a timer |
27
+ | **Always-fresh auto refresh** | ✅ Sidebar card refreshes balance & costs every 60s; opening the popover triggers an immediate refresh | Some always-on badges refresh on a timer |
28
28
  | **Peak/off-peak pricing** | ✅ Built-in official 2026-08-17 rate table, auto-switches by window | Partial support |
29
29
  | **Security** | ✅ API key stays in the host process; loopback-only guard | Varies |
30
30
 
@@ -32,17 +32,18 @@ A balance & cost widget for the [DeepSeek Harness](https://github.com/deepseek-a
32
32
 
33
33
  ## Features
34
34
 
35
- - **Account balance** — On click, the host proxies DeepSeek's official `GET /user/balance` and shows the `¥` balance. The API key is resolved through the host credentials service and never leaves the host process; the browser only talks to same-origin routes.
36
- - **Session cost (estimate)** — Computed from the session's `tokenUsage` projection × DeepSeek's official peak/off-peak price table. Follows the configured model (default `deepseek-v4-flash`, switchable to `deepseek-v4-pro`) and the Beijing-time peak/off-peak windows automatically.
37
- - **Last prompt cost (estimate)** — Parses the current session file and prices the last turn's token usage, answering "how much did that last prompt cost".
38
- - **Today total cost (estimate)** — Walks every session under `~/.dsh/sessions/` and sums today's (calendar day) token usage × price.
35
+ - **Account balance** — On click, the host proxies DeepSeek's official `GET /user/balance` and shows the `¥` balance; the balance number is color-coded by threshold (healthy / amber below `lowThreshold` / red below `criticalThreshold`). The API key is resolved through the host credentials service and never leaves the host process; the browser only talks to same-origin routes.
36
+ - **Last prompt cost (estimate)** — Parses the most recent session file and prices the last turn's token usage, answering "how much did that last prompt cost", with the **session name** labeled underneath.
37
+ - **Today · this session cost (estimate)** — The current session's usage today (calendar day) × DeepSeek's official peak/off-peak price table. Follows the configured model (default `deepseek-v4-flash`, switchable to `deepseek-v4-pro`) and the Beijing-time peak/off-peak windows automatically.
38
+ - **Today · this workspace cost (estimate)** — Sums today's token usage × price across every session in the current workspace (anchored by the current session).
39
+ - **Today · all workspaces cost (estimate)** — Walks every session under `~/.dsh/sessions/` and sums today's (calendar day) token usage × price.
40
+ - **Peak/off-peak status** — The card and popover borders are tinted by the current window (orange at peak / green at off-peak), a "Peak/Off-peak" tag sits next to the popover title, and hovering it shows the current price tier (input/output per 1M tokens).
39
41
  - **Token usage** — Also shows the session's input (incl. cache hits) / output tokens.
40
42
  - **One-click top-up** — a "Top up" link in the popover footer jumps to the official DeepSeek top-up page (platform.deepseek.com/top_up) in a new tab.
41
- - **Sidebar card** — a persistent card at the sidebar footer shows balance, a remaining-ratio bar, and today's total; globally visible, refreshes every 60s.
42
- - **Remaining-ratio bar** — blue (healthy) → amber (below lowThreshold) → red (below criticalThreshold).
43
+ - **Sidebar card** — a persistent card at the sidebar footer shows balance and today's cost; globally visible, auto-refreshes every 60s (balance via the official API, costs parsed locally), and opening the popover triggers an immediate refresh.
44
+ - **Balance color warning** — the balance number is tinted in three tiers: default (healthy) → amber (below `lowThreshold`) → red (below `criticalThreshold`).
43
45
  - **Official price auto-sync** — fetches the DeepSeek official pricing page on startup and every 12h; falls back to built-in rates on failure.
44
46
  - **Agent tool** — a `deepseek_billing` tool lets the model answer "how much balance do I have / how much did today cost".
45
- - **On-demand refresh** — No polling, no background requests; endpoints are only hit when you click the icon. Costs zero tokens to use.
46
47
 
47
48
  ## Why this plugin
48
49
 
@@ -55,15 +56,15 @@ A balance & cost widget for the [DeepSeek Harness](https://github.com/deepseek-a
55
56
  host half (lib/index.js)
56
57
  ctx.webServer.register:
57
58
  GET /api/dsh-balance/balance → official /user/balance (loopback-only guard)
58
- GET /api/dsh-balance/active-cost → last prompt + session total (most recent session)
59
- GET /api/dsh-balance/today-cost → today's costs (dual: current workspace + all)
59
+ GET /api/dsh-balance/active-cost → last prompt + today-this-session (with session name, most recent session)
60
+ GET /api/dsh-balance/today-cost → today's costs (dual: current workspace + all workspaces)
60
61
  Zero @deepseek-ai/* imports; loads from any profile layout.
61
62
  Also registers a deepseek_billing tool for model-driven queries.
62
63
 
63
64
  client half (lib/client.js)
64
65
  ctx.slots.inject("sidebar.footer.action")
65
- → persistent sidebar footer card (balance + ratio bar + today)
66
- → click opens four-tier cost popover + ⓘ term explanations
66
+ → persistent sidebar footer card (balance + today's cost, peak-tinted border)
67
+ → click opens five-tier cost popover + peak tag + ⓘ term explanations
67
68
  ```
68
69
 
69
70
  ## Installation
@@ -152,6 +153,23 @@ Built-in DeepSeek official peak/off-peak pricing (CNY per 1M tokens), effective
152
153
 
153
154
  ## Changelog
154
155
 
156
+ ### v0.5.1 — Cold-start speedup & cleanup
157
+ - 🚀 **Performance**: today-cost cold start dropped from ~5.4s to ~0.01s — only session files modified today are decompressed (mtime filter), parsed results are cached by (path, mtime), and parsing runs in parallel
158
+ - ⏰ **Refresh wording**: README now matches the code — the persistent card auto-refreshes every 60s (the old "no polling" claims contradicted the code and have been corrected)
159
+ - 🌏 **Timezone fix**: peak/off-peak windows are now computed in Beijing time (UTC+8) explicitly instead of the host's local timezone
160
+ - 🧹 **Cleanup**: removed a dead client-side pricing stack (PRICING/priceSession) that was never called; all pricing now goes through the host
161
+ - 📐 **Pricing parsing hardened**: peak rates are parsed explicitly from the official page (no more hard-coded "off-peak × 2"); if a cache-hit rate cannot be parsed the whole table falls back to built-in rates instead of silently pricing cache hits at 0
162
+ - 🏷️ **Card label**: the card footer now reads "Today · all" to make clear it is the global (all-workspaces) figure
163
+ - 🎨 **Visual refresh**: SVG wallet icon replaces 💰, press/pop micro-animations, muted secondary tiers in the popover
164
+
165
+ ### v0.5.0 — Five-tier costs & peak/off-peak status
166
+ - ✨ **Added**: the cost breakdown is now five tiers — balance / last prompt / today-this-session / today-this-workspace / today-all-workspaces
167
+ - The last-prompt row labels the **session name** underneath (based on the most recent session)
168
+ - "Today · this session" = the current session's usage today; "Today · this workspace" = all sessions in the current workspace today (workspace anchored by the current session); "Today · all workspaces" = everything across all workspaces today
169
+ - ✨ **Added**: peak/off-peak status visuals — card and popover borders tinted by window (orange at peak / green at off-peak), a "Peak/Off-peak" tag next to the popover title with a hover tooltip showing the current price tier
170
+ - 🎨 **Changed**: removed the remaining-ratio bar; the balance number is now color-coded directly by threshold (healthy / amber / red)
171
+ - 🗑️ **Removed**: the "This session" row (all-time session total) from the popover
172
+
155
173
  ### v0.2.0 — Last prompt & today total cost
156
174
  - ✨ **Added**: popover now shows "last prompt cost" and "today total cost"
157
175
  - Last prompt: prices the current session's last turn from the session file
@@ -166,7 +184,9 @@ Built-in DeepSeek official peak/off-peak pricing (CNY per 1M tokens), effective
166
184
 
167
185
  - Config tree: `dsh --profile web --dump-config` should show a `balance-widget` entry.
168
186
  - Balance route: after restarting dsh web, `curl -s http://127.0.0.1:3080/api/dsh-balance/balance` should return `{ ok, balance_infos, modelId }`.
169
- - Cost routes: `curl -s "http://127.0.0.1:3080/api/dsh-balance/last-cost?session=SESSION_ID"` and `curl -s http://127.0.0.1:3080/api/dsh-balance/today-cost` should return `{ cost, inputTokens, outputTokens, modelId }`.
187
+ - Session costs: `curl -s http://127.0.0.1:3080/api/dsh-balance/active-cost` should return `{ lastPrompt, todaySession, title, sessionId, peak, workspaceName, ... }`; append `?session=SESSION_ID` to target a specific session.
188
+ - Today costs: `curl -s http://127.0.0.1:3080/api/dsh-balance/today-cost` should return `{ workspace: { cost, ..., cwd }, all: { cost, ... }, modelId }`.
189
+ - Legacy route: `curl -s "http://127.0.0.1:3080/api/dsh-balance/last-cost?session=SESSION_ID"` still works and returns `{ cost, inputTokens, outputTokens, modelId }`.
170
190
 
171
191
  ## License
172
192
 
package/README.md CHANGED
@@ -9,11 +9,11 @@
9
9
  [![Node 24](https://img.shields.io/badge/Node%2024-ready-brightgreen?style=flat-square)](https://nodejs.org)
10
10
  [![Zero deps](https://img.shields.io/badge/dependencies-zero-brightgreen?style=flat-square)]()
11
11
 
12
- DeepSeek Harness (DSH) Web GUI 的余额与成本小部件:侧边栏底部常驻卡片显示账户余额与剩余比例条,点击弹出四层级成本明细(最近提问 / 本会话 / 今日·本项目 / 今日·全部)。
12
+ DeepSeek Harness (DSH) Web GUI 的余额与成本小部件:侧边栏底部常驻卡片显示账户余额与今日花费,点击弹出五层级成本明细(余额 / 最近提问·标注会话名 / 今日·本会话 / 今日·本工作区 / 今日·所有工作区)。
13
13
 
14
14
  ## 效果预览
15
15
 
16
- | 侧边栏卡片(左下角常驻) | 点击弹出四层级成本 |
16
+ | 侧边栏卡片(左下角常驻) | 点击弹出五层级成本 |
17
17
  | --- | --- |
18
18
  | ![侧边栏卡片](docs/screenshot-corner.png) | ![成本明细弹框](docs/screenshot-popover.png) |
19
19
 
@@ -24,7 +24,7 @@ DeepSeek Harness (DSH) Web GUI 的余额与成本小部件:侧边栏底部常
24
24
  | **零外部依赖** | ✅ 不 import 任何 `@deepseek-ai/*` 包,无原生模块 | ❌ 多数依赖 dsh SDK 包 |
25
25
  | **Node 24 兼容** | ✅ 天然兼容(零依赖设计),任何 profile 布局可加载 | ⚠️ 不少社区插件在 Node 24 下报错 |
26
26
  | **启动稳定性** | ✅ 用官方 `ctx.webServer` 注册路由,不与 apiproxy 冲突 | ⚠️ 有的自建 HTTP 服务导致 dsh web 启动崩溃 |
27
- | **按需查询** | ✅ 点击才查余额,无轮询、零后台请求 | 有的常驻徽章定时刷新 |
27
+ | **常驻自动刷新** | ✅ 侧边栏卡片每 60s 自动刷新余额与成本,弹框点击即强刷 | 有的常驻徽章定时刷新 |
28
28
  | **峰谷定价** | ✅ 内置官方 2026-08-17 峰谷价表,自动按时段切换 | 部分支持 |
29
29
  | **安全性** | ✅ API key 仅在宿主进程,loopback-only 守卫 | 参差不齐 |
30
30
 
@@ -32,17 +32,18 @@ DeepSeek Harness (DSH) Web GUI 的余额与成本小部件:侧边栏底部常
32
32
 
33
33
  ## 功能
34
34
 
35
- - **账户余额** — 点击图标时经宿主代理查询 DeepSeek 官方 `GET /user/balance`,展示 `¥` 余额;API key 只在宿主进程内读取(凭据服务),浏览器不接触密钥。
36
- - **本会话成本(估算)** — 由会话的 `tokenUsage` 投影 × DeepSeek 官方峰谷定价表计算,随当前会话模型(默认 `deepseek-v4-flash`,可在配置中改为 `deepseek-v4-pro`)与北京时间高峰/空闲时段自动切换。
37
- - **最近一次提问成本(估算)** — 从当前会话文件解析最后一个 turn 的 token 用量 × 单价,回答"刚才那条提问花了多少"。
38
- - **今天总成本(估算)** — 遍历 `~/.dsh/sessions/` 下所有会话,累加今天(自然日)的 token 用量 × 单价。
39
- - **Token 用量** — 同时展示本会话输入(含缓存命中)/ 输出 token 数。
35
+ - **账户余额** — 点击图标时经宿主代理查询 DeepSeek 官方 `GET /user/balance`,展示 `¥` 余额;余额数字按阈值自动变色(充足 / 低于 `lowThreshold` 变黄 / 低于 `criticalThreshold` 变红);API key 只在宿主进程内读取(凭据服务),浏览器不接触密钥。
36
+ - **最近一次提问成本(估算)** — 从最近活跃会话文件解析最后一个 turn 的 token 用量 × 单价,回答"刚才那条提问花了多少";下方标注该会话的**会话名**。
37
+ - **今日·本会话成本(估算)** — 当前会话今天(自然日)产生的 token 用量 × DeepSeek 官方峰谷定价表计算,随当前会话模型(默认 `deepseek-v4-flash`,可在配置中改为 `deepseek-v4-pro`)与北京时间高峰/空闲时段自动切换。
38
+ - **今日·本工作区成本(估算)** — 遍历当前工作区(由当前会话锚定)下的所有会话,累加今天的 token 用量 × 单价。
39
+ - **今日·所有工作区成本(估算)** — 遍历 `~/.dsh/sessions/` 下所有工作区的所有会话,累加今天的 token 用量 × 单价。
40
+ - **峰谷状态标签** — 卡片与弹框边框按当前时段着色(峰时橙色 / 谷时绿色),弹框标题旁显示「峰时/谷时」标签,悬停可查看当前价格档位(输入/输出单价)。
41
+ - **Token 用量** — 同时展示输入(含缓存命中)/ 输出 token 数。
40
42
  - **一键充值** — 弹层底部「去充值」链接直达 DeepSeek 官方充值页(platform.deepseek.com/top_up),新窗口打开。
41
- - **侧边栏常驻卡片** — 侧边栏底部(设置上方)显示余额 + 剩余比例条 + 今日花费,全局可见,60 秒自动刷新。
42
- - **余额剩余比例条** — 蓝色(充足)→ 琥珀(低于 lowThreshold)→ 红色(低于 criticalThreshold)三档。
43
+ - **侧边栏常驻卡片** — 侧边栏底部(设置上方)显示余额 + 今日花费,全局可见;每 60 秒自动刷新(余额走官方接口、成本为本地会话解析),打开弹框时也会立即刷新一次,无需手动操作。
44
+ - **余额数字变色预警** — 余额数字按三档着色:充足(默认色)→ 琥珀(低于 `lowThreshold`)→ 红色(低于 `criticalThreshold`),一眼判断余额健康度。
43
45
  - **官方价格自动同步** — 启动时 + 每 12 小时抓取 DeepSeek 官方定价页,改价自动跟进;失败回退内置价目表。
44
46
  - **模型工具查询** — 新增 `deepseek_billing` 工具,可直接问模型"余额多少/今天花了多少"。
45
- - **按需刷新** — 无轮询、无后台请求;只有点击图标时才发起查询,不消耗任何 token。
46
47
 
47
48
  ## 架构
48
49
 
@@ -50,15 +51,15 @@ DeepSeek Harness (DSH) Web GUI 的余额与成本小部件:侧边栏底部常
50
51
  host 半区 (lib/index.js)
51
52
  ctx.webServer.register:
52
53
  GET /api/dsh-balance/balance → 官方 /user/balance(loopback-only 守卫)
53
- GET /api/dsh-balance/active-cost → 最近活跃会话的最近提问 + 会话总计
54
- GET /api/dsh-balance/today-cost → 今日成本(双值:当前工作区 + 全部)
54
+ GET /api/dsh-balance/active-cost → 最近活跃会话的最近提问 + 今日·本会话(含会话名)
55
+ GET /api/dsh-balance/today-cost → 今日成本(双值:当前工作区 + 所有工作区)
55
56
  依赖:零外部 @deepseek-ai/* import,任何 profile 布局均可解析
56
57
  另有 deepseek_billing 工具供模型直接查询余额/成本
57
58
 
58
59
  client 半区 (lib/client.js)
59
60
  ctx.slots.inject("sidebar.footer.action")
60
- → 侧边栏底部常驻卡片(余额 + 比例条 + 今日)
61
- → 点击弹出四层级成本明细 + ⓘ 名词解释
61
+ → 侧边栏底部常驻卡片(余额 + 今日花费,峰/谷时段描边着色)
62
+ → 点击弹出五层级成本明细 + 峰谷标签 + ⓘ 名词解释
62
63
  ```
63
64
 
64
65
  ## 安装
@@ -149,6 +150,23 @@ DSH 的插件配置统一放在这个文件里:
149
150
 
150
151
  ## 版本历史
151
152
 
153
+ ### v0.5.1 — 冷启动提速与代码清理
154
+ - 🚀 **性能**:今日成本冷启动从 ~5.4s 降至 ~0.01s——只解压当天活跃的会话文件(按 mtime 过滤)+ 解压结果按 (path, mtime) 长期缓存 + 并行解析
155
+ - ⏰ **轮询口径**:README 与代码统一——常驻卡片每 60s 自动刷新(此前文档"无轮询"表述自相矛盾,已更正)
156
+ - 🌏 **峰谷时区修正**:峰/谷判断改为显式按北京时间(UTC+8)计算,不再依赖宿主机时区
157
+ - 🧹 **清理**:移除客户端一套从未调用的死计价代码(PRICING/priceSession),计价统一走宿主
158
+ - 📐 **定价解析加固**:峰价改为从官方页面显式解析(不再硬编码"谷价 ×2");解析不到缓存命中价时整表回退内置价目表,避免静默按 0 计价
159
+ - 🏷️ **卡片语义**:卡片底部"今日"改为"今日·全部",明确是全局(所有工作区)口径
160
+ - 🎨 **外观**:💰 换 SVG 钱包图标、弹框/卡片微交互动画、层级弱化(详情见 v0.5.1 diff)
161
+
162
+ ### v0.5.0 — 五层级成本与峰谷状态
163
+ - ✨ **新增**:成本明细改为五层级——余额 / 最近一次提问 / 今日·本会话 / 今日·本工作区 / 今日·所有工作区
164
+ - 最近一次提问下方标注**会话名**(基于最近活跃会话)
165
+ - 「今日·本会话」= 当前会话今天产生的费用;「今日·本工作区」= 当前工作区今天所有会话合计(由当前会话锚定工作区);「今日·所有工作区」= 全部工作区今天合计
166
+ - ✨ **新增**:峰/谷时段状态可视化——卡片与弹框边框按时段着色(峰时橙色 / 谷时绿色),弹框标题旁显示「峰时/谷时」标签,悬停查看当前价格档位
167
+ - 🎨 **调整**:移除余额剩余比例条,改为余额数字按阈值直接变色(充足 / 黄 / 红)
168
+ - 🗑️ **移除**:弹框中的「本会话成本」(会话全程累计)行
169
+
152
170
  ### v0.2.0 — 最近一次提问与今日总成本
153
171
  - ✨ **新增**:弹层增加「最近一次提问成本」与「今天总成本」两项
154
172
  - 最近一次提问:解析当前会话最后一个 turn 的 token 用量 × 单价
@@ -163,7 +181,9 @@ DSH 的插件配置统一放在这个文件里:
163
181
 
164
182
  - 配置树:`dsh --profile web --dump-config` 应出现 `balance-widget` 条目
165
183
  - 余额路由:重启 dsh web 后 `curl -s http://127.0.0.1:3080/api/dsh-balance/balance` 应返回 `{ ok, balance_infos, modelId }`
166
- - 成本路由:`curl -s "http://127.0.0.1:3080/api/dsh-balance/last-cost?session=SESSION_ID"` 与 `curl -s http://127.0.0.1:3080/api/dsh-balance/today-cost` 应返回 `{ cost, inputTokens, outputTokens, modelId }`
184
+ - 会话成本:`curl -s http://127.0.0.1:3080/api/dsh-balance/active-cost` 应返回 `{ lastPrompt, todaySession, title, sessionId, peak, workspaceName, ... }`;加 `?session=SESSION_ID` 可指定会话
185
+ - 今日成本:`curl -s http://127.0.0.1:3080/api/dsh-balance/today-cost` 应返回 `{ workspace: { cost, ..., cwd }, all: { cost, ... }, modelId }`
186
+ - 兼容旧路由:`curl -s "http://127.0.0.1:3080/api/dsh-balance/last-cost?session=SESSION_ID"` 仍可用,返回 `{ cost, inputTokens, outputTokens, modelId }`
167
187
 
168
188
  ## License
169
189
 
Binary file
Binary file
package/lib/client.js CHANGED
@@ -8,7 +8,7 @@ window.__ModuleLoader__.load({
8
8
  let react = require("react");
9
9
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
10
  //#region lib/types/client/styles.js
11
- const css = ".dshbw_wrap{position:relative;display:inline-flex;align-items:center}.dshbw_btn{background:none;border:none;cursor:pointer;color:var(--dsw-alias-label-secondary);display:inline-flex;align-items:center;gap:4px;padding:2px 6px;border-radius:6px;font-size:12px;line-height:18px;font-family:inherit}.dshbw_btn:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.dshbw_btn[data-level=\"1\"]{color:var(--dsw-alias-state-warn-primary)}.dshbw_btn[data-level=\"2\"]{color:var(--dsw-alias-state-error-primary)}.dshbw_pop{position:absolute;top:calc(100% + 6px);right:0;z-index:60;min-width:230px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l3);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.18);padding:12px 14px;font-size:12px;line-height:20px;color:var(--dsw-alias-label-primary)}.dshbw_head{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px}.dshbw_title{font-weight:600;color:var(--dsw-alias-label-primary)}.dshbw_refresh{background:none;border:1px solid var(--dsw-alias-border-l2);cursor:pointer;color:var(--dsw-alias-label-secondary);display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:6px;font-size:13px;line-height:1;padding:0;font-family:inherit}.dshbw_refresh:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}.dshbw_refresh[data-spinning=true]{animation:dshbw-spin .8s linear infinite}.dshbw_row{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:2px 0}.dshbw_label{display:inline-flex;align-items:center;min-width:0}.dshbw_val{font-weight:600;color:var(--dsw-alias-label-primary)}.dshbw_val[data-level=\"1\"]{color:var(--dsw-alias-state-warn-primary)}.dshbw_val[data-level=\"2\"]{color:var(--dsw-alias-state-error-primary)}.dshbw_err{color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px;margin-top:6px}.dshbw_hint{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;margin-top:6px;border-top:1px solid var(--dsw-alias-border-l1);padding-top:6px}.dshbw_foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:8px}.dshbw_link{background:none;border:none;cursor:pointer;color:var(--dsw-alias-state-business-primary);display:inline-flex;align-items:center;gap:4px;padding:2px 6px;border-radius:6px;font-size:12px;line-height:18px;font-family:inherit;text-decoration:none}.dshbw_link:hover{color:var(--dsw-alias-state-business-primary);text-decoration:underline}@keyframes dshbw-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}.dshbw_side{display:flex;flex-direction:column;gap:4px;padding:6px 8px;margin:0 8px 4px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}.dshbw_side:hover{background:var(--dsw-alias-interactive-bg-hover)}.dshbw_sideTop{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12px;line-height:18px}.dshbw_sideAmount{font-weight:600}.dshbw_sideAmount[data-level=\"1\"]{color:var(--dsw-alias-state-warn-primary)}.dshbw_sideAmount[data-level=\"2\"]{color:var(--dsw-alias-state-error-primary)}.dshbw_bar{height:4px;border-radius:2px;background:var(--dsw-alias-border-l2);overflow:hidden}.dshbw_barFill{height:100%;border-radius:2px;transition:width .4s ease}.dshbw_barFill[data-level=\"0\"]{background:var(--dsw-alias-state-business-primary)}.dshbw_barFill[data-level=\"1\"]{background:var(--dsw-alias-state-warn-primary)}.dshbw_barFill[data-level=\"2\"]{background:var(--dsw-alias-state-error-primary)}.dshbw_sideFoot{display:flex;justify-content:space-between;font-size:10px;line-height:14px;color:var(--dsw-alias-label-tertiary)}.dshbw_sideWrap{position:relative}.dshbw_side{display:flex;flex-direction:column;gap:4px;width:100%;padding:6px 8px;margin:0 8px 4px;border:none;border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);cursor:pointer;font-family:inherit;text-align:left}.dshbw_side:hover{background:var(--dsw-alias-interactive-bg-hover)}.dshbw_sidePop{position:absolute;bottom:calc(100% + 8px);left:8px;z-index:70;min-width:240px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l3);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.18);padding:12px 14px;font-size:12px;line-height:20px;color:var(--dsw-alias-label-primary)}.dshbw_sidePopHead{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.dshbw_sidePopTitle{font-weight:600}.dshbw_sidePopClose{background:none;border:none;cursor:pointer;color:var(--dsw-alias-label-tertiary);font-size:16px;line-height:1;padding:0 2px;font-family:inherit}.dshbw_sidePopClose:hover{color:var(--dsw-alias-label-primary)}.dshbw_sidePopFoot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:10px}.dshbw_sidePopRefresh{background:none;border:1px solid var(--dsw-alias-border-l2);cursor:pointer;color:var(--dsw-alias-label-secondary);display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:6px;padding:0;font-family:inherit}.dshbw_sidePopRefresh:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}";
11
+ const css = ".dshbw_row{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:2px 0}.dshbw_label{display:inline-flex;align-items:center;min-width:0}.dshbw_val{font-weight:600;color:var(--dsw-alias-label-primary)}.dshbw_val[data-tier=\"sec\"]{font-weight:400;color:var(--dsw-alias-label-secondary)}.dshbw_val[data-tier=\"ter\"]{font-weight:400;color:var(--dsw-alias-label-tertiary)}.dshbw_val[data-level=\"1\"]{color:var(--dsw-alias-state-warn-primary)}.dshbw_val[data-level=\"2\"]{color:var(--dsw-alias-state-error-primary)}.dshbw_err{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:16px;margin-top:6px}.dshbw_link{background:none;border:none;cursor:pointer;color:var(--dsw-alias-state-business-primary);display:inline-flex;align-items:center;gap:4px;padding:2px 6px;border-radius:6px;font-size:12px;line-height:18px;font-family:inherit;text-decoration:none}.dshbw_link:hover{color:var(--dsw-alias-state-business-primary);text-decoration:underline}.dshbw_sideTop{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12px;line-height:18px}.dshbw_sideAmount{font-weight:600}.dshbw_sideAmount[data-level=\"1\"]{color:var(--dsw-alias-state-warn-primary);font-weight:600}.dshbw_sideAmount[data-level=\"2\"]{color:var(--dsw-alias-state-error-primary)}.dshbw_sideFoot{display:flex;justify-content:space-between;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary)}.dshbw_sideWrap{position:relative}.dshbw_side{display:flex;flex-direction:column;gap:4px;width:100%;padding:6px 8px;margin:0 8px 4px;border:none;border-radius:6px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);cursor:pointer;font-family:inherit;text-align:left;transition:transform 120ms cubic-bezier(0.23,1,0.32,1),background 120ms cubic-bezier(0.23,1,0.32,1)}.dshbw_side:active{transform:scale(.97)}.dshbw_side:hover{background:var(--dsw-alias-interactive-bg-hover)}.dshbw_sidePop{position:absolute;bottom:calc(100% + 8px);left:8px;z-index:70;min-width:240px;background:var(--dsw-alias-bg-layer-3);border:none;border-radius:12px;box-shadow:0 0 1px rgba(0,0,0,.3),0 4px 14px rgba(0,0,0,.1);padding:12px 12px;font-size:12px;line-height:20px;color:var(--dsw-alias-label-primary);animation:dshbw-pop-in 160ms cubic-bezier(0.23,1,0.32,1) both}.dshbw_sidePopHead{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.dshbw_sidePopTitle{font-weight:600}.dshbw_sidePopClose{background:none;border:none;cursor:pointer;color:var(--dsw-alias-label-tertiary);font-size:16px;line-height:1;padding:0 2px;font-family:inherit}.dshbw_sidePopClose:hover{color:var(--dsw-alias-label-primary)}.dshbw_sidePopFoot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:10px}.dshbw_sidePopRefresh{background:none;border:1px solid var(--dsw-alias-border-l2);cursor:pointer;color:var(--dsw-alias-label-secondary);display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:6px;padding:0;font-family:inherit}.dshbw_sidePopRefresh:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}.dshbw_side[data-peak=\"true\"]{border:1px solid var(--dsw-alias-state-warn-primary)}.dshbw_side[data-peak=\"false\"]{border:1px solid var(--dsw-alias-state-success-primary)}.dshbw_peakTag{display:inline-flex;align-items:center;gap:4px;font-size:12px;line-height:16px;padding:1px 7px;border-radius:6px}.dshbw_peakTag[data-peak=\"true\"]{color:var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-state-warn-tertiary)}.dshbw_peakTag[data-peak=\"false\"]{color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-state-success-tertiary)}.dshbw_sessionNote{display:flex;align-items:center;gap:4px;margin-top:6px;padding-top:6px;border-top:1px solid var(--dsw-alias-border-l1);font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);min-width:0}.dshbw_sessionName{max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dshbw_ico{color:var(--dsw-alias-state-business-primary);display:inline-flex;align-items:center}@keyframes dshbw-pop-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.dshbw_sidePopRefresh,.dshbw_sidePopClose{transition:transform 120ms cubic-bezier(0.23,1,0.32,1),color 120ms cubic-bezier(0.23,1,0.32,1)}.dshbw_sidePopRefresh:active{transform:scale(.92)}.dshbw_sidePopClose:active{transform:scale(.92)}";
12
12
  const tagId = "dsh-balance-widget/styles.css";
13
13
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
14
14
  const tag = document.createElement("style");
@@ -18,113 +18,33 @@ window.__ModuleLoader__.load({
18
18
  document.head.appendChild(tag);
19
19
  }
20
20
  var styles = {
21
- "wrap": "dshbw_wrap",
22
- "btn": "dshbw_btn",
23
- "pop": "dshbw_pop",
24
- "head": "dshbw_head",
25
- "title": "dshbw_title",
26
- "refresh": "dshbw_refresh",
27
21
  "row": "dshbw_row",
28
22
  "label": "dshbw_label",
29
23
  "val": "dshbw_val",
30
24
  "err": "dshbw_err",
31
- "hint": "dshbw_hint",
32
- "foot": "dshbw_foot",
33
25
  "link": "dshbw_link",
34
26
  "side": "dshbw_side",
35
27
  "sideWrap": "dshbw_sideWrap",
36
28
  "sideTop": "dshbw_sideTop",
37
29
  "sideAmount": "dshbw_sideAmount",
38
- "bar": "dshbw_bar",
39
- "barFill": "dshbw_barFill",
40
30
  "sideFoot": "dshbw_sideFoot",
41
31
  "sidePop": "dshbw_sidePop",
42
32
  "sidePopHead": "dshbw_sidePopHead",
43
33
  "sidePopTitle": "dshbw_sidePopTitle",
44
34
  "sidePopClose": "dshbw_sidePopClose",
45
35
  "sidePopFoot": "dshbw_sidePopFoot",
46
- "sidePopRefresh": "dshbw_sidePopRefresh"
36
+ "sidePopRefresh": "dshbw_sidePopRefresh",
37
+ "peakTag": "dshbw_peakTag",
38
+ "sessionNote": "dshbw_sessionNote",
39
+ "sessionName": "dshbw_sessionName"
47
40
  };
48
41
  //#endregion
49
- //#region lib/types/client/pricing.js
50
- /**
51
- * DeepSeek official pricing, in CNY per 1M tokens, effective 2026-08-17
52
- * (peak/off-peak two-tier billing). Peak windows are Beijing time:
53
- * 09:00-12:00 and 14:00-18:00; off-peak is half the peak price.
54
- * Prices are hard-coded from the official announcement and are estimates
55
- * for display — actual billing is authoritative from the provider.
56
- */
57
- const PRICING = {
58
- "deepseek-v4-flash": {
59
- peak: { inputMiss: 3.0, inputHit: 0.10, output: 9.0 },
60
- offPeak: { inputMiss: 1.5, inputHit: 0.05, output: 4.5 }
61
- },
62
- "deepseek-v4-pro": {
63
- peak: { inputMiss: 9.0, inputHit: 0.30, output: 27.0 },
64
- offPeak: { inputMiss: 4.5, inputHit: 0.15, output: 13.5 }
65
- },
66
- "deepseek-chat": {
67
- peak: { inputMiss: 3.0, inputHit: 0.10, output: 9.0 },
68
- offPeak: { inputMiss: 1.5, inputHit: 0.05, output: 4.5 }
69
- },
70
- "deepseek-reasoner": {
71
- peak: { inputMiss: 9.0, inputHit: 0.30, output: 27.0 },
72
- offPeak: { inputMiss: 4.5, inputHit: 0.15, output: 13.5 }
73
- }
74
- };
75
- /** Fall back to Flash pricing for unknown model ids. */
76
- function pricingFor(modelId) {
77
- return PRICING[modelId] ?? PRICING["deepseek-v4-flash"];
78
- }
79
- /**
80
- * Whether `date` falls inside a Beijing-time peak window.
81
- * The host's local timezone is used; the widget is for a CN deployment.
82
- */
83
- function isPeak(date) {
84
- const hour = date.getHours();
85
- return (hour >= 9 && hour < 12) || (hour >= 14 && hour < 18);
86
- }
87
- /**
88
- * Price one session's token usage into a CNY estimate.
89
- * @param usage - tokenUsage projection value.
90
- * @param modelId - provider model id used for pricing.
91
- * @returns { input, output, total } in CNY, or null when usage is absent.
92
- */
93
- function priceSession(usage, modelId) {
94
- if (usage === void 0 || usage === null) return null;
95
- const input = usage.uncachedInputTokens ?? 0;
96
- const hit = usage.cacheReadTokens ?? 0;
97
- const write = usage.cacheWriteTokens ?? 0;
98
- const output = usage.outputTokens ?? 0;
99
- if (input + hit + write + output <= 0) return null;
100
- const table = isPeak(new Date()) ? pricingFor(modelId).peak : pricingFor(modelId).offPeak;
101
- const inputCost = (input * table.inputMiss + (hit + write) * table.inputHit) / 1e6;
102
- const outputCost = (output * table.output) / 1e6;
103
- return {
104
- input: inputCost,
105
- output: outputCost,
106
- total: inputCost + outputCost
107
- };
108
- }
109
42
  /** Format a CNY amount: ¥ + 3 significant decimals (drop trailing zeros). */
110
43
  function formatCny(value) {
111
44
  if (value === void 0 || value === null || !Number.isFinite(value)) return "—";
112
45
  const rounded = Math.round(value * 1e4) / 1e4;
113
46
  return `¥${rounded.toLocaleString("zh-CN", { minimumFractionDigits: 0, maximumFractionDigits: 4 })}`;
114
47
  }
115
- /** Format token counts compactly: 1234 -> 1.2K. */
116
- function formatTokens(value) {
117
- if (value === void 0 || value === null) return "—";
118
- if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
119
- if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
120
- return String(value);
121
- }
122
- /** Format an epoch ms as HH:MM local time. */
123
- function formatTime(ms) {
124
- if (ms === void 0 || ms === null) return "—";
125
- const d = new Date(ms);
126
- return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
127
- }
128
48
  //#endregion
129
49
  //#region lib/types/client/balance-api.js
130
50
  /** Same-origin balance route (host half registers it over ctx.webServer). */
@@ -136,15 +56,29 @@ window.__ModuleLoader__.load({
136
56
  /** Official DeepSeek platform top-up page (opens in a new tab). */
137
57
  const TOP_UP_URL = "https://platform.deepseek.com/top_up";
138
58
  /**
139
- * Fetch the active session's costs (last prompt + session total).
140
- * @returns { ok, lastPrompt?, sessionTotal?, error? }
59
+ * Fetch the active session's costs (last prompt + today's session total).
60
+ * @param {string} [sessionId] - the currently selected session; when omitted
61
+ * the host falls back to its most recently active session.
62
+ * @returns { ok, lastPrompt?, todaySession?, sessionTitle?, workspaceName?, peak?, error? }
141
63
  */
142
- async function fetchActiveCost() {
64
+ async function fetchActiveCost(sessionId) {
143
65
  try {
144
- const response = await fetch(ACTIVE_COST_ROUTE, { headers: { accept: "application/json" } });
66
+ const url = sessionId === void 0 || sessionId === ""
67
+ ? ACTIVE_COST_ROUTE
68
+ : `${ACTIVE_COST_ROUTE}?session=${encodeURIComponent(sessionId)}`;
69
+ const response = await fetch(url, { headers: { accept: "application/json" } });
145
70
  const payload = await response.json().catch(() => ({}));
146
71
  if (!response.ok) return { ok: false, error: payload.error ?? `active-cost responded ${response.status}` };
147
- return { ok: true, lastPrompt: payload.lastPrompt?.cost ?? 0, sessionTotal: payload.sessionTotal?.cost ?? 0 };
72
+ return {
73
+ ok: true,
74
+ lastPrompt: payload.lastPrompt?.cost ?? 0,
75
+ todaySession: payload.todaySession?.cost ?? 0,
76
+ sessionTitle: payload.title,
77
+ workspaceName: payload.workspaceName,
78
+ cwd: payload.cwd,
79
+ sessionId: payload.sessionId,
80
+ peak: payload.peak
81
+ };
148
82
  } catch (error) {
149
83
  return { ok: false, error: error instanceof Error ? error.message : String(error) };
150
84
  }
@@ -238,20 +172,30 @@ window.__ModuleLoader__.load({
238
172
  * cost, top-up, refresh) using only global data.
239
173
  * @param {Object} props - wide (sidebar expanded), t (locale).
240
174
  */
241
- function SidebarBalanceCard({ wide, t }) {
175
+ function SidebarBalanceCard({ wide, t, useSessions }) {
242
176
  const [open, setOpen] = (0, react.useState)(false);
243
177
  const [balance, setBalance] = (0, react.useState)(null);
244
178
  const [lastPrompt, setLastPrompt] = (0, react.useState)(null);
245
- const [sessionTotal, setSessionTotal] = (0, react.useState)(null);
179
+ const [todaySession, setTodaySession] = (0, react.useState)(null);
180
+ const [sessionTitle, setSessionTitle] = (0, react.useState)(null);
246
181
  const [todayWs, setTodayWs] = (0, react.useState)(null);
247
182
  const [todayAll, setTodayAll] = (0, react.useState)(null);
248
183
  const [lowThreshold, setLowThreshold] = (0, react.useState)(5);
249
184
  const [criticalThreshold, setCriticalThreshold] = (0, react.useState)(1);
250
185
  const [error, setError] = (0, react.useState)(null);
251
186
  const [loading, setLoading] = (0, react.useState)(false);
187
+ const [peak, setPeak] = (0, react.useState)(null);
188
+ // The currently selected session id (the session the user is viewing).
189
+ // This anchors "today · this workspace" to the actual current workspace.
190
+ const currentSessionId = typeof useSessions === "function"
191
+ ? (0, useSessions)((s) => s.current)
192
+ : void 0;
252
193
  /** Fetch today's costs (workspace + all) from the dual-value route. */
253
194
  const fetchTodayCosts = async () => {
254
- const response = await fetch(TODAY_COST_ROUTE, { headers: { accept: "application/json" } });
195
+ const url = currentSessionId !== void 0 && currentSessionId !== ""
196
+ ? `${TODAY_COST_ROUTE}?session=${encodeURIComponent(currentSessionId)}`
197
+ : TODAY_COST_ROUTE;
198
+ const response = await fetch(url, { headers: { accept: "application/json" } });
255
199
  const payload = await response.json().catch(() => ({}));
256
200
  if (!response.ok) return { ok: false, error: payload.error ?? `today-cost responded ${response.status}` };
257
201
  return {
@@ -265,7 +209,7 @@ window.__ModuleLoader__.load({
265
209
  setError(null);
266
210
  const [bResult, aResult, tResult] = await Promise.all([
267
211
  fetchBalance(),
268
- fetchActiveCost(),
212
+ fetchActiveCost(currentSessionId),
269
213
  fetchTodayCosts()
270
214
  ]);
271
215
  const failures = [];
@@ -278,7 +222,9 @@ window.__ModuleLoader__.load({
278
222
  }
279
223
  if (aResult.ok) {
280
224
  setLastPrompt(aResult.lastPrompt);
281
- setSessionTotal(aResult.sessionTotal);
225
+ setTodaySession(aResult.todaySession);
226
+ setSessionTitle(aResult.sessionTitle ?? null);
227
+ setPeak(aResult.peak ?? null);
282
228
  } else {
283
229
  failures.push(`会话: ${aResult.error}`);
284
230
  }
@@ -295,7 +241,7 @@ window.__ModuleLoader__.load({
295
241
  refresh(false);
296
242
  const timer = setInterval(() => refresh(false), 60000);
297
243
  return () => clearInterval(timer);
298
- }, []);
244
+ }, [currentSessionId]);
299
245
  // Close the popover when clicking outside the card.
300
246
  const wrapRef = (0, react.useRef)(null);
301
247
  (0, react.useEffect)(() => {
@@ -308,11 +254,6 @@ window.__ModuleLoader__.load({
308
254
  return () => document.removeEventListener("mousedown", onDocClick);
309
255
  }, [open]);
310
256
  const level = balance === null ? 0 : balance.total < criticalThreshold ? 2 : balance.total < lowThreshold ? 1 : 0;
311
- // Remaining-ratio: 100% at lowThreshold (or above), 0% at 0.
312
- let percent = 100;
313
- if (balance !== null && balance.total < lowThreshold) {
314
- percent = lowThreshold <= 0 ? 100 : Math.max(0, Math.round(balance.total / lowThreshold * 100));
315
- }
316
257
  const toggle = async () => {
317
258
  const next = !open;
318
259
  setOpen(next);
@@ -325,28 +266,21 @@ window.__ModuleLoader__.load({
325
266
  children: [(0, react_jsx_runtime.jsx)("button", {
326
267
  type: "button",
327
268
  className: styles.side,
269
+ "data-peak": peak !== null ? String(peak.active) : undefined,
328
270
  title: t("title"),
329
271
  onClick: toggle,
330
272
  children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
331
273
  children: [(0, react_jsx_runtime.jsx)("div", {
332
274
  className: styles.sideTop,
333
275
  children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
334
- children: [(0, react_jsx_runtime.jsx)("span", {
335
- children: "💰"
276
+ children: [(0, react_jsx_runtime.jsx)("span", { className: "dshbw_ico",
277
+ children: (0, react_jsx_runtime.jsx)("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round", children: [(0, react_jsx_runtime.jsx)("rect", { x: 3, y: 6, width: 18, height: 13, rx: 2 }), (0, react_jsx_runtime.jsx)("path", { d: "M3 10h18" })] })
336
278
  }), (0, react_jsx_runtime.jsx)("span", {
337
279
  className: styles.sideAmount,
338
280
  "data-level": level > 0 ? level : undefined,
339
281
  children: balance !== null ? `${balance.symbol}${balance.total.toFixed(2)}` : "—"
340
282
  })]
341
283
  })
342
- }), (0, react_jsx_runtime.jsx)("div", {
343
- className: styles.bar,
344
- "aria-label": t("side.bar"),
345
- children: (0, react_jsx_runtime.jsx)("div", {
346
- className: styles.barFill,
347
- "data-level": level,
348
- style: { width: `${percent}%` }
349
- })
350
284
  }), (0, react_jsx_runtime.jsx)("div", {
351
285
  className: styles.sideFoot,
352
286
  children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
@@ -360,6 +294,7 @@ window.__ModuleLoader__.load({
360
294
  })
361
295
  }), open && (0, react_jsx_runtime.jsx)("div", {
362
296
  className: styles.sidePop,
297
+ "data-peak": peak !== null ? String(peak.active) : undefined,
363
298
  role: "dialog",
364
299
  children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
365
300
  children: [(0, react_jsx_runtime.jsx)("div", {
@@ -367,7 +302,20 @@ window.__ModuleLoader__.load({
367
302
  children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
368
303
  children: [(0, react_jsx_runtime.jsx)("span", {
369
304
  className: styles.sidePopTitle,
370
- children: t("title")
305
+ children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
306
+ children: [t("title"), peak !== null && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
307
+ label: peak.active
308
+ ? t("peak.tooltip.active", { input: peak.inputMiss, output: peak.output })
309
+ : t("peak.tooltip.offpeak", { input: peak.inputMiss, output: peak.output }),
310
+ side: "top",
311
+ delayMs: 200,
312
+ children: (0, react_jsx_runtime.jsx)("span", {
313
+ className: styles.peakTag,
314
+ "data-peak": String(peak.active),
315
+ children: peak.active ? t("peak.active") : t("peak.offpeak")
316
+ })
317
+ })]
318
+ })
371
319
  }), (0, react_jsx_runtime.jsx)("button", {
372
320
  type: "button",
373
321
  className: styles.sidePopClose,
@@ -409,24 +357,35 @@ window.__ModuleLoader__.load({
409
357
  })]
410
358
  })
411
359
  }), (0, react_jsx_runtime.jsx)("span", {
412
- className: styles.val,
360
+ className: styles.val, "data-tier": "ter",
413
361
  children: loading ? "…" : lastPrompt !== null ? formatCny(lastPrompt) : "—"
414
362
  })]
415
363
  })
364
+ }), sessionTitle !== null && sessionTitle !== "" && (0, react_jsx_runtime.jsx)("div", {
365
+ className: styles.sessionNote,
366
+ title: sessionTitle,
367
+ children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
368
+ children: [(0, react_jsx_runtime.jsx)("span", {
369
+ children: t("sessionNote")
370
+ }), (0, react_jsx_runtime.jsx)("span", {
371
+ className: styles.sessionName,
372
+ children: sessionTitle
373
+ })]
374
+ })
416
375
  }), (0, react_jsx_runtime.jsx)("div", {
417
376
  className: styles.row,
418
377
  children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
419
378
  children: [(0, react_jsx_runtime.jsx)("span", {
420
379
  className: styles.label,
421
380
  children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
422
- children: [t("sessionTotal"), (0, react_jsx_runtime.jsx)(ExplainIcon, {
423
- label: t("explain.sessionTotal"),
381
+ children: [t("todaySession"), (0, react_jsx_runtime.jsx)(ExplainIcon, {
382
+ label: t("explain.todaySession"),
424
383
  t
425
384
  })]
426
385
  })
427
386
  }), (0, react_jsx_runtime.jsx)("span", {
428
387
  className: styles.val,
429
- children: loading ? "…" : sessionTotal !== null ? formatCny(sessionTotal) : "—"
388
+ children: loading ? "…" : todaySession !== null ? formatCny(todaySession) : "—"
430
389
  })]
431
390
  })
432
391
  }), (0, react_jsx_runtime.jsx)("div", {
@@ -441,7 +400,7 @@ window.__ModuleLoader__.load({
441
400
  })]
442
401
  })
443
402
  }), (0, react_jsx_runtime.jsx)("span", {
444
- className: styles.val,
403
+ className: styles.val, "data-tier": "sec",
445
404
  children: loading ? "…" : todayWs !== null ? formatCny(todayWs) : "—"
446
405
  })]
447
406
  })
@@ -457,7 +416,7 @@ window.__ModuleLoader__.load({
457
416
  })]
458
417
  })
459
418
  }), (0, react_jsx_runtime.jsx)("span", {
460
- className: styles.val,
419
+ className: styles.val, "data-tier": "sec",
461
420
  children: loading ? "…" : todayAll !== null ? formatCny(todayAll) : "—"
462
421
  })]
463
422
  })
@@ -501,21 +460,25 @@ window.__ModuleLoader__.load({
501
460
  "refresh.title": "刷新余额与成本",
502
461
  "balance": "账户余额",
503
462
  "lastPrompt": "最近一次提问",
504
- "sessionTotal": "本会话成本",
505
- "todayWs": "今日·本项目",
506
- "todayAll": "今日·全部",
463
+ "todaySession": "今日·本会话",
464
+ "todayWs": "今日·本工作区",
465
+ "todayAll": "今日·所有工作区",
507
466
  "explain.aria": "解释",
508
467
  "explain.balance": "DeepSeek 账户可用余额,实时查询官方接口。",
509
468
  "explain.lastPrompt": "最近活跃会话的最后一个问题(含 AI 回复)的费用估算。",
510
- "explain.sessionTotal": "最近活跃会话从开始到现在的累计费用估算。",
469
+ "explain.todaySession": "当前会话今天产生的费用估算(从会话开始累计到今天)。",
511
470
  "explain.todayWs": "当前工作区今天所有会话的费用合计估算。",
512
471
  "explain.todayAll": "所有工作区今天所有会话的费用合计估算(含其他项目)。",
513
472
  "warn.title.1": "余额不足 ¥5,建议及时充值",
514
473
  "warn.title.2": "余额低于 ¥1,即将耗尽!",
515
474
  "warn.text.1": "⚠️ 余额不足 ¥5,建议及时充值。",
516
475
  "warn.text.2": "⛔ 余额低于 ¥1,即将耗尽,请立即充值!",
517
- "side.bar": "余额剩余比例",
518
- "todayCostShort": "今日",
476
+ "todayCostShort": "今日·全部",
477
+ "peak.active": "峰时",
478
+ "peak.offpeak": "谷时",
479
+ "peak.tooltip.active": "当前为峰时(09:00-12:00、14:00-18:00,价格翻倍):输入 ¥{input}/M · 输出 ¥{output}/M",
480
+ "peak.tooltip.offpeak": "当前为谷时(其余时段,半价):输入 ¥{input}/M · 输出 ¥{output}/M",
481
+ "sessionNote": "基于最近活跃会话:",
519
482
  "hint": "价格为估算值,实际以官方账单为准。",
520
483
  "topUp": "去充值",
521
484
  "close": "关闭"
@@ -528,21 +491,25 @@ window.__ModuleLoader__.load({
528
491
  "refresh.title": "Refresh balance and costs",
529
492
  "balance": "Account balance",
530
493
  "lastPrompt": "Last prompt",
531
- "sessionTotal": "This session",
532
- "todayWs": "Today · this project",
533
- "todayAll": "Today · all",
494
+ "todaySession": "Today · this session",
495
+ "todayWs": "Today · this workspace",
496
+ "todayAll": "Today · all workspaces",
534
497
  "explain.aria": "Explain",
535
498
  "explain.balance": "DeepSeek account balance, fetched live from the official API.",
536
499
  "explain.lastPrompt": "Estimated cost of the latest question (incl. the AI reply) in the most recent session.",
537
- "explain.sessionTotal": "Estimated total cost of the most recent session since it started.",
500
+ "explain.todaySession": "Estimated cost of today's usage in the current session.",
538
501
  "explain.todayWs": "Estimated total cost of today's sessions in the current workspace.",
539
502
  "explain.todayAll": "Estimated total cost of today's sessions across all workspaces.",
540
503
  "warn.title.1": "Balance below ¥5, consider topping up",
541
504
  "warn.title.2": "Balance below ¥1, almost exhausted!",
542
505
  "warn.text.1": "⚠️ Balance below ¥5, consider topping up.",
543
506
  "warn.text.2": "⛔ Balance below ¥1, almost exhausted — top up now!",
544
- "side.bar": "Balance remaining ratio",
545
- "todayCostShort": "Today",
507
+ "todayCostShort": "Today · all",
508
+ "peak.active": "Peak",
509
+ "peak.offpeak": "Off-peak",
510
+ "peak.tooltip.active": "Peak hours now (09:00-12:00, 14:00-18:00, 2× price): input ¥{input}/M · output ¥{output}/M",
511
+ "peak.tooltip.offpeak": "Off-peak hours now (rest of day, half price): input ¥{input}/M · output ¥{output}/M",
512
+ "sessionNote": "Based on most recent session:",
546
513
  "hint": "Prices are estimates; actual billing from the provider is authoritative.",
547
514
  "topUp": "Top up",
548
515
  "close": "Close"
package/lib/index.js CHANGED
@@ -246,9 +246,15 @@ const PRICING_ALIASES = {
246
246
  "deepseek-official": "deepseek-v4-flash"
247
247
  };
248
248
 
249
- /** Whether `date` falls inside a Beijing-time peak window. */
249
+ /**
250
+ * Whether `date` falls inside a Beijing-time (UTC+8) peak window.
251
+ * DeepSeek's peak pricing applies to Beijing time 09:00-12:00 and 14:00-18:00
252
+ * regardless of the host's timezone, so the window is computed in UTC+8
253
+ * explicitly instead of the machine's local time.
254
+ */
250
255
  function isPeak(date) {
251
- const hour = date.getHours();
256
+ const beijing = new Date(date.getTime() + 8 * 60 * 60 * 1000);
257
+ const hour = beijing.getUTCHours();
252
258
  return (hour >= 9 && hour < 12) || (hour >= 14 && hour < 18);
253
259
  }
254
260
 
@@ -330,30 +336,40 @@ function extractModelBlock(html, model) {
330
336
  }
331
337
 
332
338
  /**
333
- * Parse per-1M-token rates from a block of HTML text. Looks for numbers near
334
- * "输入"/"缓存命中"/"输出" (CN page) or "input"/"cache"/"output" (EN page),
335
- * preferring the off-peak row. Returns { offPeak, peak } or null.
339
+ * Parse per-1M-token rates from a block of HTML text. The pricing table lists
340
+ * rows per metric with "空闲时段 <off-peak> … 高峰时段 <peak>" columns (CN) or
341
+ * "off-peak … peak" (EN); the first model column is the one we price. Each
342
+ * metric is captured as an (offPeak, peak) pair from its labelled row.
343
+ * Returns { offPeak, peak } or null when any off-peak figure is missing
344
+ * (the caller then falls back to the built-in rate table rather than silently
345
+ * pricing cache hits at 0).
336
346
  */
337
347
  function parseRates(block) {
338
348
  // Strip tags to plain text for simpler matching.
339
349
  const text = block.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
340
- const num = (pattern) => {
341
- const m = text.match(pattern);
342
- if (!m) return void 0;
343
- const v = parseFloat(m[1].replace(/[¥$,\s]/g, ""));
344
- return Number.isFinite(v) ? v : void 0;
350
+ const pair = (label) => {
351
+ // label + short gap + "空闲时段 <n>" + gap + "高峰时段 <n>" (first model
352
+ // column). Also tolerate EN "off-peak … peak".
353
+ const re = new RegExp(
354
+ label + "[\\s\\S]{0,120}?(?:空闲时段|off[- ]?peak)\\s*([\\d.]+)[\\s\\S]{0,80}?(?:高峰时段|peak)\\s*([\\d.]+)",
355
+ "i"
356
+ );
357
+ const m = text.match(re);
358
+ if (!m) return null;
359
+ const a = parseFloat(m[1]);
360
+ const b = parseFloat(m[2]);
361
+ return Number.isFinite(a) && Number.isFinite(b) ? [a, b] : null;
345
362
  };
346
- // Try patterns for off-peak (空闲/off-peak) first, then any.
347
- const offPeakInputMiss = num(/缓存未命中[^\d]*([\d.]+)/) ?? num(/cache[\s-]*miss[^\d]*([\d.]+)/);
348
- const offPeakInputHit = num(/缓存命中[^\d]*([\d.]+)/) ?? num(/cache[\s-]*hit[^\d]*([\d.]+)/);
349
- const offPeakOutput = num(/输出[^\d]*([\d.]+)/) ?? num(/output[^\d]*([\d.]+)/);
350
- if (offPeakInputMiss === void 0 || offPeakOutput === void 0) return null;
351
- const peakInputMiss = (offPeakInputMiss ?? 0) * 2;
352
- const peakInputHit = (offPeakInputHit ?? 0) * 2;
353
- const peakOutput = (offPeakOutput ?? 0) * 2;
363
+ const hit = pair("缓存命中");
364
+ const miss = pair("缓存未命中");
365
+ const out = pair("百万tokens输出") ?? pair("输出");
366
+ if (miss === null || out === null) return null;
367
+ // Cache-hit row missing is acceptable only if it cannot be parsed at all —
368
+ // reject rather than guess zero.
369
+ if (hit === null) return null;
354
370
  return {
355
- peak: { inputMiss: peakInputMiss, inputHit: peakInputHit, output: peakOutput },
356
- offPeak: { inputMiss: offPeakInputMiss, inputHit: offPeakInputHit ?? 0, output: offPeakOutput }
371
+ offPeak: { inputMiss: miss[0], inputHit: hit[0], output: out[0] },
372
+ peak: { inputMiss: miss[1], inputHit: hit[1], output: out[1] }
357
373
  };
358
374
  }
359
375
 
@@ -461,6 +477,7 @@ function parseSessionUsages(text) {
461
477
  const samples = [];
462
478
  let currentModel = void 0;
463
479
  let cwd = void 0;
480
+ let title = void 0;
464
481
  for (const line of text.split("\n")) {
465
482
  if (line === "") continue;
466
483
  let event;
@@ -472,6 +489,8 @@ function parseSessionUsages(text) {
472
489
  const type = event.type;
473
490
  if (type === "session") {
474
491
  cwd = event.cwd;
492
+ } else if (type === "session/title") {
493
+ title = event.data?.title ?? title;
475
494
  } else if (type === "request/header") {
476
495
  currentModel = event.data?.header?.config?.model ?? currentModel;
477
496
  } else if (type === "assistant/message") {
@@ -486,7 +505,7 @@ function parseSessionUsages(text) {
486
505
  }
487
506
  }
488
507
  }
489
- return { samples, cwd };
508
+ return { samples, cwd, title };
490
509
  }
491
510
 
492
511
  /** Cost of the last turn (the most recent user prompt) in one session file. */
@@ -506,12 +525,16 @@ function lastTurnCost(samples) {
506
525
  return { cost, inputTokens: input, outputTokens: output };
507
526
  }
508
527
 
509
- /** Total cost across every sample of one session (all turns). */
510
- function sessionTotalCost(samples) {
528
+ /** Cost of one session's usage that falls on the current calendar day. */
529
+ function todaySessionCost(samples) {
530
+ const now = new Date();
531
+ const dayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
532
+ const dayEnd = dayStart + 24 * 60 * 60 * 1000;
511
533
  let cost = 0;
512
534
  let input = 0;
513
535
  let output = 0;
514
536
  for (const s of samples) {
537
+ if (s.time < dayStart || s.time >= dayEnd) continue;
515
538
  cost += priceUsage(s.usage, s.model, s.time);
516
539
  input += (s.usage.inputTokens ?? s.usage.uncachedInputTokens ?? 0) + (s.usage.cacheReadTokens ?? 0);
517
540
  output += s.usage.outputTokens ?? 0;
@@ -580,7 +603,34 @@ function todayCost(sessionList, cwdFilter) {
580
603
  return { cost, inputTokens: input, outputTokens: output };
581
604
  }
582
605
 
583
- /** Walk every session file under the sessions root, decompress, and parse. */
606
+ /**
607
+ * Decompressed session parse cache, keyed by path and validated against the
608
+ * file's mtime. Long-lived: an untouched file is never re-decompressed, so a
609
+ * cold /today-cost after the outer 30s TTL expires only re-reads files that
610
+ * actually changed. Cleared wholesale when it outgrows a bound.
611
+ */
612
+ const sessionParseCache = new Map();
613
+ const SESSION_PARSE_CACHE_MAX = 600;
614
+
615
+ /** Decompress+parse one file, honoring the (path,mtime) cache. */
616
+ async function parseSessionFileCached(path, mtimeMs) {
617
+ const hit = sessionParseCache.get(path);
618
+ if (hit !== void 0 && hit.mtimeMs === mtimeMs) return hit.parsed;
619
+ const result = await decompressSession(path);
620
+ if (!result.ok) return null;
621
+ const parsed = parseSessionUsages(result.text);
622
+ if (sessionParseCache.size >= SESSION_PARSE_CACHE_MAX) sessionParseCache.clear();
623
+ sessionParseCache.set(path, { mtimeMs, parsed });
624
+ return parsed;
625
+ }
626
+
627
+ /**
628
+ * Walk every session file under the sessions root and parse the ones that can
629
+ * contribute to "today" numbers. Only files whose mtime falls on the current
630
+ * local calendar day can contain today's samples, so older files are skipped
631
+ * entirely (avoids decompressing the whole ~/.dsh/sessions tree on every cold
632
+ * call). Parsing runs in parallel; results are cached by (path, mtime).
633
+ */
584
634
  async function collectAllSessionSamples() {
585
635
  const root = sessionsRoot();
586
636
  const fs = (await awaitImportFs());
@@ -597,14 +647,17 @@ async function collectAllSessionSamples() {
597
647
  } catch {
598
648
  return [];
599
649
  }
600
- const perSession = [];
650
+ const now = new Date();
651
+ const dayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
652
+ const candidates = [];
601
653
  for (const file of files) {
602
- const result = await decompressSession(file);
603
- if (!result.ok) continue;
604
- const parsed = parseSessionUsages(result.text);
605
- perSession.push(parsed);
654
+ try {
655
+ const mtimeMs = fs.statSync(file).mtimeMs;
656
+ if (mtimeMs >= dayStart) candidates.push({ path: file, mtimeMs });
657
+ } catch { /* file vanished mid-walk */ }
606
658
  }
607
- return perSession;
659
+ const parsed = await Promise.all(candidates.map((c) => parseSessionFileCached(c.path, c.mtimeMs)));
660
+ return parsed.filter((p) => p !== null);
608
661
  }
609
662
 
610
663
  /** Simple in-memory TTL cache for cost computations (30s). */
@@ -695,24 +748,36 @@ function makeRoutes(ctx, config) {
695
748
  path: API.todayCost,
696
749
  handler: async (req, res) => {
697
750
  if (!guard(req, res, "GET")) return;
698
- const key = "today";
751
+ const url = new URL(req.url ?? "/", "http://localhost");
752
+ const sessionId = url.searchParams.get("session") ?? "";
753
+ const key = `today:${sessionId}`;
699
754
  const compute = async () => {
700
755
  const sessionList = await collectAllSessionSamples();
701
756
  const all = todayCost(sessionList);
702
- // "Current workspace": the cwd of the session with the most
703
- // samples today (best proxy for where the user is working).
757
+ // "Current workspace": prefer the cwd of the explicitly passed
758
+ // session (the client's currently selected session). Fall back
759
+ // to the session with the most samples today when absent.
704
760
  let activeCwd = void 0;
705
- let activeCount = 0;
706
- for (const session of sessionList) {
707
- if (session.cwd === void 0) continue;
708
- const todaySamples = session.samples.filter((s) => {
709
- const now = new Date();
710
- const dayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
711
- return s.time >= dayStart && s.time < dayStart + 24 * 60 * 60 * 1000;
712
- }).length;
713
- if (todaySamples > activeCount) {
714
- activeCount = todaySamples;
715
- activeCwd = session.cwd;
761
+ if (sessionId !== "") {
762
+ const target = await resolveSessionFile(sessionId);
763
+ if (target !== void 0) {
764
+ const text = await decompressSession(target);
765
+ if (text.ok) activeCwd = parseSessionUsages(text.text).cwd;
766
+ }
767
+ }
768
+ if (activeCwd === void 0) {
769
+ let activeCount = 0;
770
+ for (const session of sessionList) {
771
+ if (session.cwd === void 0) continue;
772
+ const todaySamples = session.samples.filter((s) => {
773
+ const now = new Date();
774
+ const dayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
775
+ return s.time >= dayStart && s.time < dayStart + 24 * 60 * 60 * 1000;
776
+ }).length;
777
+ if (todaySamples > activeCount) {
778
+ activeCount = todaySamples;
779
+ activeCwd = session.cwd;
780
+ }
716
781
  }
717
782
  }
718
783
  const workspace = todayCost(sessionList, activeCwd);
@@ -732,7 +797,13 @@ function makeRoutes(ctx, config) {
732
797
  path: API.activeCost,
733
798
  handler: async (req, res) => {
734
799
  if (!guard(req, res, "GET")) return;
735
- const file = await findMostRecentSessionFile();
800
+ const url = new URL(req.url ?? "/", "http://localhost");
801
+ const sessionId = url.searchParams.get("session") ?? "";
802
+ // Prefer the explicitly passed session (the client's currently
803
+ // selected one); fall back to the most recently active session.
804
+ const file = sessionId !== ""
805
+ ? await resolveSessionFile(sessionId)
806
+ : await findMostRecentSessionFile();
736
807
  if (file === void 0) {
737
808
  writeJson(res, 404, { error: "no session files found", modelId: config.modelId });
738
809
  return;
@@ -744,11 +815,36 @@ function makeRoutes(ctx, config) {
744
815
  }
745
816
  const parsed = parseSessionUsages(result.text);
746
817
  const last = lastTurnCost(parsed.samples);
747
- const total = sessionTotalCost(parsed.samples);
818
+ const todaySession = todaySessionCost(parsed.samples);
819
+ // Current peak/off-peak state + price tier for the configured model.
820
+ const now = new Date();
821
+ const peakActive = isPeak(now);
822
+ const table = peakActive
823
+ ? pricingTable[pricingKey(config.modelId)].peak
824
+ : pricingTable[pricingKey(config.modelId)].offPeak;
825
+ // Short workspace name from the cwd path (last segment).
826
+ let workspaceName = "未知工作区";
827
+ if (typeof parsed.cwd === "string" && parsed.cwd !== "") {
828
+ const segments = parsed.cwd.split("/").filter((s) => s !== "");
829
+ workspaceName = segments.length > 0 ? segments[segments.length - 1] : parsed.cwd;
830
+ }
831
+ // Session id from the file path (…/session-<id>/session.jsonl.zstd).
832
+ let fileSessionId = void 0;
833
+ const sidMatch = file.match(/session-([0-9a-f-]+)/);
834
+ if (sidMatch !== null) fileSessionId = sidMatch[1];
748
835
  writeJson(res, 200, {
749
836
  lastPrompt: last,
750
- sessionTotal: total,
837
+ todaySession,
751
838
  cwd: parsed.cwd,
839
+ title: parsed.title,
840
+ workspaceName,
841
+ sessionId: fileSessionId,
842
+ peak: {
843
+ active: peakActive,
844
+ inputMiss: table.inputMiss,
845
+ inputHit: table.inputHit,
846
+ output: table.output
847
+ },
752
848
  modelId: config.modelId,
753
849
  pricingSource,
754
850
  pricingSyncedAt
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-balance-widget",
3
- "description": "Balance & cost widget for the dsh web GUI: a sidebar footer card shows the DeepSeek account balance, a remaining-ratio bar and today's costs; clicking opens a popover with four-tier cost breakdown (last prompt / this session / today-this-project / today-all), term explanations, and a one-click top-up link. Zero external dependencies — works on Node 24.",
4
- "version": "0.4.5",
3
+ "description": "Balance & cost widget for the dsh web GUI: a sidebar footer card shows the DeepSeek account balance and today's costs; clicking opens a popover with a five-tier cost breakdown (last prompt with session name / today-this-session / today-this-workspace / today-all-workspaces), peak/off-peak status tags, term explanations, and a one-click top-up link. Zero external dependencies — works on Node 24.",
4
+ "version": "0.5.1",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "files": [