dsh-balance-widget 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -9,13 +9,13 @@
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 💰 icon in the corner of the conversation session header. Click it to see your DeepSeek account balance and the current session's estimated spend.
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
- | Balance always visible | Popover on click |
16
+ | Sidebar card | Five-tier cost popover |
17
17
  | --- | --- |
18
- | ![Balance corner](docs/screenshot-corner.png) | ![Popover detail](docs/screenshot-popover.png) |
18
+ | ![Sidebar card](docs/screenshot-corner.png) | ![Cost popover](docs/screenshot-popover.png) |
19
19
 
20
20
  ## How it differs from similar plugins
21
21
 
@@ -32,14 +32,16 @@ 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, refreshes every 60s.
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
47
  - **On-demand refresh** — No polling, no background requests; endpoints are only hit when you click the icon. Costs zero tokens to use.
@@ -54,14 +56,16 @@ A balance & cost widget for the [DeepSeek Harness](https://github.com/deepseek-a
54
56
  ```
55
57
  host half (lib/index.js)
56
58
  ctx.webServer.register:
57
- GET /api/dsh-balance/balance → official /user/balance (loopback-only guard)
58
- GET /api/dsh-balance/last-cost → last prompt cost (session file + zstd decode)
59
- GET /api/dsh-balance/today-cost → today's total across all sessions
59
+ GET /api/dsh-balance/balance → official /user/balance (loopback-only guard)
60
+ GET /api/dsh-balance/active-cost → last prompt + today-this-session (with session name, most recent session)
61
+ GET /api/dsh-balance/today-cost → today's costs (dual: current workspace + all workspaces)
62
+ Zero @deepseek-ai/* imports; loads from any profile layout.
63
+ Also registers a deepseek_billing tool for model-driven queries.
60
64
 
61
65
  client half (lib/client.js)
62
- ctx.slots.inject("conversation.session.header.utilities")
63
- → 💰 icon (session header, top-right corner)
64
- → click fetches same-origin APIs → popover with balance + three costs + tokens
66
+ ctx.slots.inject("sidebar.footer.action")
67
+ → persistent sidebar footer card (balance + today's cost, peak-tinted border)
68
+ → click opens five-tier cost popover + peak tag + ⓘ term explanations
65
69
  ```
66
70
 
67
71
  ## Installation
@@ -150,6 +154,14 @@ Built-in DeepSeek official peak/off-peak pricing (CNY per 1M tokens), effective
150
154
 
151
155
  ## Changelog
152
156
 
157
+ ### v0.5.0 — Five-tier costs & peak/off-peak status
158
+ - ✨ **Added**: the cost breakdown is now five tiers — balance / last prompt / today-this-session / today-this-workspace / today-all-workspaces
159
+ - The last-prompt row labels the **session name** underneath (based on the most recent session)
160
+ - "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
161
+ - ✨ **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
162
+ - 🎨 **Changed**: removed the remaining-ratio bar; the balance number is now color-coded directly by threshold (healthy / amber / red)
163
+ - 🗑️ **Removed**: the "This session" row (all-time session total) from the popover
164
+
153
165
  ### v0.2.0 — Last prompt & today total cost
154
166
  - ✨ **Added**: popover now shows "last prompt cost" and "today total cost"
155
167
  - Last prompt: prices the current session's last turn from the session file
package/README.md CHANGED
@@ -9,13 +9,13 @@
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
- | ![余额常驻](docs/screenshot-corner.png) | ![弹框详情](docs/screenshot-popover.png) |
18
+ | ![侧边栏卡片](docs/screenshot-corner.png) | ![成本明细弹框](docs/screenshot-popover.png) |
19
19
 
20
20
  ## 与同类插件的区别
21
21
 
@@ -32,14 +32,16 @@ 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
47
  - **按需刷新** — 无轮询、无后台请求;只有点击图标时才发起查询,不消耗任何 token。
@@ -49,15 +51,16 @@ DeepSeek Harness (DSH) Web GUI 的余额与成本小部件:在会话头部右
49
51
  ```
50
52
  host 半区 (lib/index.js)
51
53
  ctx.webServer.register:
52
- GET /api/dsh-balance/balance → 官方 /user/balance(loopback-only 守卫)
53
- GET /api/dsh-balance/last-cost → 当前会话最近一次提问成本(读会话文件 + zstd 解压)
54
- GET /api/dsh-balance/today-cost → 今天所有会话总成本
54
+ GET /api/dsh-balance/balance → 官方 /user/balance(loopback-only 守卫)
55
+ GET /api/dsh-balance/active-cost → 最近活跃会话的最近提问 + 今日·本会话(含会话名)
56
+ GET /api/dsh-balance/today-cost → 今日成本(双值:当前工作区 + 所有工作区)
55
57
  依赖:零外部 @deepseek-ai/* import,任何 profile 布局均可解析
58
+ 另有 deepseek_billing 工具供模型直接查询余额/成本
56
59
 
57
60
  client 半区 (lib/client.js)
58
- ctx.slots.inject("conversation.session.header.utilities")
59
- → 💰 图标(会话头部右上角)
60
- → 点击 fetch 同源 API → 弹层展示余额 + 三种成本 + token
61
+ ctx.slots.inject("sidebar.footer.action")
62
+ → 侧边栏底部常驻卡片(余额 + 今日花费,峰/谷时段描边着色)
63
+ → 点击弹出五层级成本明细 + 峰谷标签 + ⓘ 名词解释
61
64
  ```
62
65
 
63
66
  ## 安装
@@ -148,6 +151,14 @@ DSH 的插件配置统一放在这个文件里:
148
151
 
149
152
  ## 版本历史
150
153
 
154
+ ### v0.5.0 — 五层级成本与峰谷状态
155
+ - ✨ **新增**:成本明细改为五层级——余额 / 最近一次提问 / 今日·本会话 / 今日·本工作区 / 今日·所有工作区
156
+ - 最近一次提问下方标注**会话名**(基于最近活跃会话)
157
+ - 「今日·本会话」= 当前会话今天产生的费用;「今日·本工作区」= 当前工作区今天所有会话合计(由当前会话锚定工作区);「今日·所有工作区」= 全部工作区今天合计
158
+ - ✨ **新增**:峰/谷时段状态可视化——卡片与弹框边框按时段着色(峰时橙色 / 谷时绿色),弹框标题旁显示「峰时/谷时」标签,悬停查看当前价格档位
159
+ - 🎨 **调整**:移除余额剩余比例条,改为余额数字按阈值直接变色(充足 / 黄 / 红)
160
+ - 🗑️ **移除**:弹框中的「本会话成本」(会话全程累计)行
161
+
151
162
  ### v0.2.0 — 最近一次提问与今日总成本
152
163
  - ✨ **新增**:弹层增加「最近一次提问成本」与「今天总成本」两项
153
164
  - 最近一次提问:解析当前会话最后一个 turn 的 token 用量 × 单价
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_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:700}.dshbw_sideAmount[data-level=\"1\"]{color:var(--dsw-alias-state-warn-primary);font-weight:700}.dshbw_sideAmount[data-level=\"2\"]{color: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)}.dshbw_side[data-peak=\"true\"]{border:1px solid var(--dsw-alias-state-warn-primary);background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, var(--dsw-alias-bg-layer-2))}.dshbw_side[data-peak=\"false\"]{border:1px solid var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, var(--dsw-alias-bg-layer-2))}.dshbw_sidePop[data-peak=\"true\"]{border-color:var(--dsw-alias-state-warn-primary)}.dshbw_sidePop[data-peak=\"false\"]{border-color:var(--dsw-alias-state-success-primary)}.dshbw_peakTag{display:inline-flex;align-items:center;gap:4px;font-size:11px;line-height:16px;padding:1px 6px;border-radius:999px}.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:11px;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}";
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");
@@ -35,15 +35,16 @@ window.__ModuleLoader__.load({
35
35
  "sideWrap": "dshbw_sideWrap",
36
36
  "sideTop": "dshbw_sideTop",
37
37
  "sideAmount": "dshbw_sideAmount",
38
- "bar": "dshbw_bar",
39
- "barFill": "dshbw_barFill",
40
38
  "sideFoot": "dshbw_sideFoot",
41
39
  "sidePop": "dshbw_sidePop",
42
40
  "sidePopHead": "dshbw_sidePopHead",
43
41
  "sidePopTitle": "dshbw_sidePopTitle",
44
42
  "sidePopClose": "dshbw_sidePopClose",
45
43
  "sidePopFoot": "dshbw_sidePopFoot",
46
- "sidePopRefresh": "dshbw_sidePopRefresh"
44
+ "sidePopRefresh": "dshbw_sidePopRefresh",
45
+ "peakTag": "dshbw_peakTag",
46
+ "sessionNote": "dshbw_sessionNote",
47
+ "sessionName": "dshbw_sessionName"
47
48
  };
48
49
  //#endregion
49
50
  //#region lib/types/client/pricing.js
@@ -136,15 +137,29 @@ window.__ModuleLoader__.load({
136
137
  /** Official DeepSeek platform top-up page (opens in a new tab). */
137
138
  const TOP_UP_URL = "https://platform.deepseek.com/top_up";
138
139
  /**
139
- * Fetch the active session's costs (last prompt + session total).
140
- * @returns { ok, lastPrompt?, sessionTotal?, error? }
140
+ * Fetch the active session's costs (last prompt + today's session total).
141
+ * @param {string} [sessionId] - the currently selected session; when omitted
142
+ * the host falls back to its most recently active session.
143
+ * @returns { ok, lastPrompt?, todaySession?, sessionTitle?, workspaceName?, peak?, error? }
141
144
  */
142
- async function fetchActiveCost() {
145
+ async function fetchActiveCost(sessionId) {
143
146
  try {
144
- const response = await fetch(ACTIVE_COST_ROUTE, { headers: { accept: "application/json" } });
147
+ const url = sessionId === void 0 || sessionId === ""
148
+ ? ACTIVE_COST_ROUTE
149
+ : `${ACTIVE_COST_ROUTE}?session=${encodeURIComponent(sessionId)}`;
150
+ const response = await fetch(url, { headers: { accept: "application/json" } });
145
151
  const payload = await response.json().catch(() => ({}));
146
152
  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 };
153
+ return {
154
+ ok: true,
155
+ lastPrompt: payload.lastPrompt?.cost ?? 0,
156
+ todaySession: payload.todaySession?.cost ?? 0,
157
+ sessionTitle: payload.title,
158
+ workspaceName: payload.workspaceName,
159
+ cwd: payload.cwd,
160
+ sessionId: payload.sessionId,
161
+ peak: payload.peak
162
+ };
148
163
  } catch (error) {
149
164
  return { ok: false, error: error instanceof Error ? error.message : String(error) };
150
165
  }
@@ -238,20 +253,30 @@ window.__ModuleLoader__.load({
238
253
  * cost, top-up, refresh) using only global data.
239
254
  * @param {Object} props - wide (sidebar expanded), t (locale).
240
255
  */
241
- function SidebarBalanceCard({ wide, t }) {
256
+ function SidebarBalanceCard({ wide, t, useSessions }) {
242
257
  const [open, setOpen] = (0, react.useState)(false);
243
258
  const [balance, setBalance] = (0, react.useState)(null);
244
259
  const [lastPrompt, setLastPrompt] = (0, react.useState)(null);
245
- const [sessionTotal, setSessionTotal] = (0, react.useState)(null);
260
+ const [todaySession, setTodaySession] = (0, react.useState)(null);
261
+ const [sessionTitle, setSessionTitle] = (0, react.useState)(null);
246
262
  const [todayWs, setTodayWs] = (0, react.useState)(null);
247
263
  const [todayAll, setTodayAll] = (0, react.useState)(null);
248
264
  const [lowThreshold, setLowThreshold] = (0, react.useState)(5);
249
265
  const [criticalThreshold, setCriticalThreshold] = (0, react.useState)(1);
250
266
  const [error, setError] = (0, react.useState)(null);
251
267
  const [loading, setLoading] = (0, react.useState)(false);
268
+ const [peak, setPeak] = (0, react.useState)(null);
269
+ // The currently selected session id (the session the user is viewing).
270
+ // This anchors "today · this workspace" to the actual current workspace.
271
+ const currentSessionId = typeof useSessions === "function"
272
+ ? (0, useSessions)((s) => s.current)
273
+ : void 0;
252
274
  /** Fetch today's costs (workspace + all) from the dual-value route. */
253
275
  const fetchTodayCosts = async () => {
254
- const response = await fetch(TODAY_COST_ROUTE, { headers: { accept: "application/json" } });
276
+ const url = currentSessionId !== void 0 && currentSessionId !== ""
277
+ ? `${TODAY_COST_ROUTE}?session=${encodeURIComponent(currentSessionId)}`
278
+ : TODAY_COST_ROUTE;
279
+ const response = await fetch(url, { headers: { accept: "application/json" } });
255
280
  const payload = await response.json().catch(() => ({}));
256
281
  if (!response.ok) return { ok: false, error: payload.error ?? `today-cost responded ${response.status}` };
257
282
  return {
@@ -265,7 +290,7 @@ window.__ModuleLoader__.load({
265
290
  setError(null);
266
291
  const [bResult, aResult, tResult] = await Promise.all([
267
292
  fetchBalance(),
268
- fetchActiveCost(),
293
+ fetchActiveCost(currentSessionId),
269
294
  fetchTodayCosts()
270
295
  ]);
271
296
  const failures = [];
@@ -278,7 +303,9 @@ window.__ModuleLoader__.load({
278
303
  }
279
304
  if (aResult.ok) {
280
305
  setLastPrompt(aResult.lastPrompt);
281
- setSessionTotal(aResult.sessionTotal);
306
+ setTodaySession(aResult.todaySession);
307
+ setSessionTitle(aResult.sessionTitle ?? null);
308
+ setPeak(aResult.peak ?? null);
282
309
  } else {
283
310
  failures.push(`会话: ${aResult.error}`);
284
311
  }
@@ -295,7 +322,7 @@ window.__ModuleLoader__.load({
295
322
  refresh(false);
296
323
  const timer = setInterval(() => refresh(false), 60000);
297
324
  return () => clearInterval(timer);
298
- }, []);
325
+ }, [currentSessionId]);
299
326
  // Close the popover when clicking outside the card.
300
327
  const wrapRef = (0, react.useRef)(null);
301
328
  (0, react.useEffect)(() => {
@@ -308,11 +335,6 @@ window.__ModuleLoader__.load({
308
335
  return () => document.removeEventListener("mousedown", onDocClick);
309
336
  }, [open]);
310
337
  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
338
  const toggle = async () => {
317
339
  const next = !open;
318
340
  setOpen(next);
@@ -325,6 +347,7 @@ window.__ModuleLoader__.load({
325
347
  children: [(0, react_jsx_runtime.jsx)("button", {
326
348
  type: "button",
327
349
  className: styles.side,
350
+ "data-peak": peak !== null ? String(peak.active) : undefined,
328
351
  title: t("title"),
329
352
  onClick: toggle,
330
353
  children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
@@ -339,14 +362,6 @@ window.__ModuleLoader__.load({
339
362
  children: balance !== null ? `${balance.symbol}${balance.total.toFixed(2)}` : "—"
340
363
  })]
341
364
  })
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
365
  }), (0, react_jsx_runtime.jsx)("div", {
351
366
  className: styles.sideFoot,
352
367
  children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
@@ -360,6 +375,7 @@ window.__ModuleLoader__.load({
360
375
  })
361
376
  }), open && (0, react_jsx_runtime.jsx)("div", {
362
377
  className: styles.sidePop,
378
+ "data-peak": peak !== null ? String(peak.active) : undefined,
363
379
  role: "dialog",
364
380
  children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
365
381
  children: [(0, react_jsx_runtime.jsx)("div", {
@@ -367,7 +383,20 @@ window.__ModuleLoader__.load({
367
383
  children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
368
384
  children: [(0, react_jsx_runtime.jsx)("span", {
369
385
  className: styles.sidePopTitle,
370
- children: t("title")
386
+ children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
387
+ children: [t("title"), peak !== null && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
388
+ label: peak.active
389
+ ? t("peak.tooltip.active", { input: peak.inputMiss, output: peak.output })
390
+ : t("peak.tooltip.offpeak", { input: peak.inputMiss, output: peak.output }),
391
+ side: "top",
392
+ delayMs: 200,
393
+ children: (0, react_jsx_runtime.jsx)("span", {
394
+ className: styles.peakTag,
395
+ "data-peak": String(peak.active),
396
+ children: peak.active ? t("peak.active") : t("peak.offpeak")
397
+ })
398
+ })]
399
+ })
371
400
  }), (0, react_jsx_runtime.jsx)("button", {
372
401
  type: "button",
373
402
  className: styles.sidePopClose,
@@ -413,20 +442,31 @@ window.__ModuleLoader__.load({
413
442
  children: loading ? "…" : lastPrompt !== null ? formatCny(lastPrompt) : "—"
414
443
  })]
415
444
  })
445
+ }), sessionTitle !== null && sessionTitle !== "" && (0, react_jsx_runtime.jsx)("div", {
446
+ className: styles.sessionNote,
447
+ title: sessionTitle,
448
+ children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
449
+ children: [(0, react_jsx_runtime.jsx)("span", {
450
+ children: t("sessionNote")
451
+ }), (0, react_jsx_runtime.jsx)("span", {
452
+ className: styles.sessionName,
453
+ children: sessionTitle
454
+ })]
455
+ })
416
456
  }), (0, react_jsx_runtime.jsx)("div", {
417
457
  className: styles.row,
418
458
  children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
419
459
  children: [(0, react_jsx_runtime.jsx)("span", {
420
460
  className: styles.label,
421
461
  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"),
462
+ children: [t("todaySession"), (0, react_jsx_runtime.jsx)(ExplainIcon, {
463
+ label: t("explain.todaySession"),
424
464
  t
425
465
  })]
426
466
  })
427
467
  }), (0, react_jsx_runtime.jsx)("span", {
428
468
  className: styles.val,
429
- children: loading ? "…" : sessionTotal !== null ? formatCny(sessionTotal) : "—"
469
+ children: loading ? "…" : todaySession !== null ? formatCny(todaySession) : "—"
430
470
  })]
431
471
  })
432
472
  }), (0, react_jsx_runtime.jsx)("div", {
@@ -501,21 +541,25 @@ window.__ModuleLoader__.load({
501
541
  "refresh.title": "刷新余额与成本",
502
542
  "balance": "账户余额",
503
543
  "lastPrompt": "最近一次提问",
504
- "sessionTotal": "本会话成本",
505
- "todayWs": "今日·本项目",
506
- "todayAll": "今日·全部",
544
+ "todaySession": "今日·本会话",
545
+ "todayWs": "今日·本工作区",
546
+ "todayAll": "今日·所有工作区",
507
547
  "explain.aria": "解释",
508
548
  "explain.balance": "DeepSeek 账户可用余额,实时查询官方接口。",
509
549
  "explain.lastPrompt": "最近活跃会话的最后一个问题(含 AI 回复)的费用估算。",
510
- "explain.sessionTotal": "最近活跃会话从开始到现在的累计费用估算。",
550
+ "explain.todaySession": "当前会话今天产生的费用估算(从会话开始累计到今天)。",
511
551
  "explain.todayWs": "当前工作区今天所有会话的费用合计估算。",
512
552
  "explain.todayAll": "所有工作区今天所有会话的费用合计估算(含其他项目)。",
513
553
  "warn.title.1": "余额不足 ¥5,建议及时充值",
514
554
  "warn.title.2": "余额低于 ¥1,即将耗尽!",
515
555
  "warn.text.1": "⚠️ 余额不足 ¥5,建议及时充值。",
516
556
  "warn.text.2": "⛔ 余额低于 ¥1,即将耗尽,请立即充值!",
517
- "side.bar": "余额剩余比例",
518
557
  "todayCostShort": "今日",
558
+ "peak.active": "峰时",
559
+ "peak.offpeak": "谷时",
560
+ "peak.tooltip.active": "当前为峰时(09:00-12:00、14:00-18:00,价格翻倍):输入 ¥{input}/M · 输出 ¥{output}/M",
561
+ "peak.tooltip.offpeak": "当前为谷时(其余时段,半价):输入 ¥{input}/M · 输出 ¥{output}/M",
562
+ "sessionNote": "基于最近活跃会话:",
519
563
  "hint": "价格为估算值,实际以官方账单为准。",
520
564
  "topUp": "去充值",
521
565
  "close": "关闭"
@@ -528,21 +572,25 @@ window.__ModuleLoader__.load({
528
572
  "refresh.title": "Refresh balance and costs",
529
573
  "balance": "Account balance",
530
574
  "lastPrompt": "Last prompt",
531
- "sessionTotal": "This session",
532
- "todayWs": "Today · this project",
533
- "todayAll": "Today · all",
575
+ "todaySession": "Today · this session",
576
+ "todayWs": "Today · this workspace",
577
+ "todayAll": "Today · all workspaces",
534
578
  "explain.aria": "Explain",
535
579
  "explain.balance": "DeepSeek account balance, fetched live from the official API.",
536
580
  "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.",
581
+ "explain.todaySession": "Estimated cost of today's usage in the current session.",
538
582
  "explain.todayWs": "Estimated total cost of today's sessions in the current workspace.",
539
583
  "explain.todayAll": "Estimated total cost of today's sessions across all workspaces.",
540
584
  "warn.title.1": "Balance below ¥5, consider topping up",
541
585
  "warn.title.2": "Balance below ¥1, almost exhausted!",
542
586
  "warn.text.1": "⚠️ Balance below ¥5, consider topping up.",
543
587
  "warn.text.2": "⛔ Balance below ¥1, almost exhausted — top up now!",
544
- "side.bar": "Balance remaining ratio",
545
588
  "todayCostShort": "Today",
589
+ "peak.active": "Peak",
590
+ "peak.offpeak": "Off-peak",
591
+ "peak.tooltip.active": "Peak hours now (09:00-12:00, 14:00-18:00, 2× price): input ¥{input}/M · output ¥{output}/M",
592
+ "peak.tooltip.offpeak": "Off-peak hours now (rest of day, half price): input ¥{input}/M · output ¥{output}/M",
593
+ "sessionNote": "Based on most recent session:",
546
594
  "hint": "Prices are estimates; actual billing from the provider is authoritative.",
547
595
  "topUp": "Top up",
548
596
  "close": "Close"
package/lib/index.js CHANGED
@@ -461,6 +461,7 @@ function parseSessionUsages(text) {
461
461
  const samples = [];
462
462
  let currentModel = void 0;
463
463
  let cwd = void 0;
464
+ let title = void 0;
464
465
  for (const line of text.split("\n")) {
465
466
  if (line === "") continue;
466
467
  let event;
@@ -472,6 +473,8 @@ function parseSessionUsages(text) {
472
473
  const type = event.type;
473
474
  if (type === "session") {
474
475
  cwd = event.cwd;
476
+ } else if (type === "session/title") {
477
+ title = event.data?.title ?? title;
475
478
  } else if (type === "request/header") {
476
479
  currentModel = event.data?.header?.config?.model ?? currentModel;
477
480
  } else if (type === "assistant/message") {
@@ -486,7 +489,7 @@ function parseSessionUsages(text) {
486
489
  }
487
490
  }
488
491
  }
489
- return { samples, cwd };
492
+ return { samples, cwd, title };
490
493
  }
491
494
 
492
495
  /** Cost of the last turn (the most recent user prompt) in one session file. */
@@ -506,12 +509,16 @@ function lastTurnCost(samples) {
506
509
  return { cost, inputTokens: input, outputTokens: output };
507
510
  }
508
511
 
509
- /** Total cost across every sample of one session (all turns). */
510
- function sessionTotalCost(samples) {
512
+ /** Cost of one session's usage that falls on the current calendar day. */
513
+ function todaySessionCost(samples) {
514
+ const now = new Date();
515
+ const dayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
516
+ const dayEnd = dayStart + 24 * 60 * 60 * 1000;
511
517
  let cost = 0;
512
518
  let input = 0;
513
519
  let output = 0;
514
520
  for (const s of samples) {
521
+ if (s.time < dayStart || s.time >= dayEnd) continue;
515
522
  cost += priceUsage(s.usage, s.model, s.time);
516
523
  input += (s.usage.inputTokens ?? s.usage.uncachedInputTokens ?? 0) + (s.usage.cacheReadTokens ?? 0);
517
524
  output += s.usage.outputTokens ?? 0;
@@ -695,24 +702,36 @@ function makeRoutes(ctx, config) {
695
702
  path: API.todayCost,
696
703
  handler: async (req, res) => {
697
704
  if (!guard(req, res, "GET")) return;
698
- const key = "today";
705
+ const url = new URL(req.url ?? "/", "http://localhost");
706
+ const sessionId = url.searchParams.get("session") ?? "";
707
+ const key = `today:${sessionId}`;
699
708
  const compute = async () => {
700
709
  const sessionList = await collectAllSessionSamples();
701
710
  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).
711
+ // "Current workspace": prefer the cwd of the explicitly passed
712
+ // session (the client's currently selected session). Fall back
713
+ // to the session with the most samples today when absent.
704
714
  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;
715
+ if (sessionId !== "") {
716
+ const target = await resolveSessionFile(sessionId);
717
+ if (target !== void 0) {
718
+ const text = await decompressSession(target);
719
+ if (text.ok) activeCwd = parseSessionUsages(text.text).cwd;
720
+ }
721
+ }
722
+ if (activeCwd === void 0) {
723
+ let activeCount = 0;
724
+ for (const session of sessionList) {
725
+ if (session.cwd === void 0) continue;
726
+ const todaySamples = session.samples.filter((s) => {
727
+ const now = new Date();
728
+ const dayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
729
+ return s.time >= dayStart && s.time < dayStart + 24 * 60 * 60 * 1000;
730
+ }).length;
731
+ if (todaySamples > activeCount) {
732
+ activeCount = todaySamples;
733
+ activeCwd = session.cwd;
734
+ }
716
735
  }
717
736
  }
718
737
  const workspace = todayCost(sessionList, activeCwd);
@@ -732,7 +751,13 @@ function makeRoutes(ctx, config) {
732
751
  path: API.activeCost,
733
752
  handler: async (req, res) => {
734
753
  if (!guard(req, res, "GET")) return;
735
- const file = await findMostRecentSessionFile();
754
+ const url = new URL(req.url ?? "/", "http://localhost");
755
+ const sessionId = url.searchParams.get("session") ?? "";
756
+ // Prefer the explicitly passed session (the client's currently
757
+ // selected one); fall back to the most recently active session.
758
+ const file = sessionId !== ""
759
+ ? await resolveSessionFile(sessionId)
760
+ : await findMostRecentSessionFile();
736
761
  if (file === void 0) {
737
762
  writeJson(res, 404, { error: "no session files found", modelId: config.modelId });
738
763
  return;
@@ -744,11 +769,36 @@ function makeRoutes(ctx, config) {
744
769
  }
745
770
  const parsed = parseSessionUsages(result.text);
746
771
  const last = lastTurnCost(parsed.samples);
747
- const total = sessionTotalCost(parsed.samples);
772
+ const todaySession = todaySessionCost(parsed.samples);
773
+ // Current peak/off-peak state + price tier for the configured model.
774
+ const now = new Date();
775
+ const peakActive = isPeak(now);
776
+ const table = peakActive
777
+ ? pricingTable[pricingKey(config.modelId)].peak
778
+ : pricingTable[pricingKey(config.modelId)].offPeak;
779
+ // Short workspace name from the cwd path (last segment).
780
+ let workspaceName = "未知工作区";
781
+ if (typeof parsed.cwd === "string" && parsed.cwd !== "") {
782
+ const segments = parsed.cwd.split("/").filter((s) => s !== "");
783
+ workspaceName = segments.length > 0 ? segments[segments.length - 1] : parsed.cwd;
784
+ }
785
+ // Session id from the file path (…/session-<id>/session.jsonl.zstd).
786
+ let fileSessionId = void 0;
787
+ const sidMatch = file.match(/session-([0-9a-f-]+)/);
788
+ if (sidMatch !== null) fileSessionId = sidMatch[1];
748
789
  writeJson(res, 200, {
749
790
  lastPrompt: last,
750
- sessionTotal: total,
791
+ todaySession,
751
792
  cwd: parsed.cwd,
793
+ title: parsed.title,
794
+ workspaceName,
795
+ sessionId: fileSessionId,
796
+ peak: {
797
+ active: peakActive,
798
+ inputMiss: table.inputMiss,
799
+ inputHit: table.inputHit,
800
+ output: table.output
801
+ },
752
802
  modelId: config.modelId,
753
803
  pricingSource,
754
804
  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.4",
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.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "files": [