@gamegeek-saikel/dsh-cost-meter 0.2.1 → 0.3.0-alpha.2

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.
Files changed (58) hide show
  1. package/README.md +9 -8
  2. package/README.zh-CN.md +9 -8
  3. package/lib/client.js +165 -93
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +114 -50
  6. package/lib/types/client/AssistantCostChip.d.ts +6 -19
  7. package/lib/types/client/AssistantCostChip.d.ts.map +1 -1
  8. package/lib/types/client/AssistantCostChip.js +74 -44
  9. package/lib/types/client/CostPluginCard.d.ts +1 -1
  10. package/lib/types/client/CostPluginCard.d.ts.map +1 -1
  11. package/lib/types/client/CostView.d.ts +1 -1
  12. package/lib/types/client/CostView.d.ts.map +1 -1
  13. package/lib/types/client/CostView.js +5 -2
  14. package/lib/types/client/SessionCostLine.d.ts +1 -4
  15. package/lib/types/client/SessionCostLine.d.ts.map +1 -1
  16. package/lib/types/client/SessionCostPill.d.ts +3 -3
  17. package/lib/types/client/SessionCostPill.d.ts.map +1 -1
  18. package/lib/types/client/SessionCostPill.js +14 -5
  19. package/lib/types/client/index.d.ts +8 -7
  20. package/lib/types/client/index.d.ts.map +1 -1
  21. package/lib/types/client/index.js +8 -2
  22. package/lib/types/client/locales.d.ts +4 -0
  23. package/lib/types/client/locales.d.ts.map +1 -1
  24. package/lib/types/client/locales.js +8 -0
  25. package/lib/types/index.d.ts +8 -3
  26. package/lib/types/index.d.ts.map +1 -1
  27. package/lib/types/index.js +25 -15
  28. package/lib/types/pricebook.d.ts +6 -0
  29. package/lib/types/pricebook.d.ts.map +1 -1
  30. package/lib/types/pricebook.js +18 -9
  31. package/lib/types/pricing.d.ts +35 -13
  32. package/lib/types/pricing.d.ts.map +1 -1
  33. package/lib/types/pricing.js +39 -17
  34. package/lib/types/session-cost-index.d.ts +61 -0
  35. package/lib/types/session-cost-index.d.ts.map +1 -0
  36. package/lib/types/session-cost-index.js +65 -0
  37. package/lib/types/session-cost-projection.d.ts +9 -8
  38. package/lib/types/session-cost-projection.d.ts.map +1 -1
  39. package/lib/types/session-cost-projection.js +11 -17
  40. package/lib/types/types.d.ts +27 -7
  41. package/lib/types/types.d.ts.map +1 -1
  42. package/package.json +33 -29
  43. package/src/client/AssistantCostChip.module.css +132 -55
  44. package/src/client/AssistantCostChip.tsx +132 -54
  45. package/src/client/CostPluginCard.tsx +1 -1
  46. package/src/client/CostView.module.css +1 -23
  47. package/src/client/CostView.tsx +6 -3
  48. package/src/client/SessionCostLine.tsx +1 -3
  49. package/src/client/SessionCostPill.module.css +14 -25
  50. package/src/client/SessionCostPill.tsx +18 -8
  51. package/src/client/index.ts +23 -9
  52. package/src/client/locales.ts +8 -0
  53. package/src/index.ts +29 -15
  54. package/src/pricebook.ts +18 -9
  55. package/src/pricing.ts +39 -17
  56. package/src/session-cost-index.ts +89 -0
  57. package/src/session-cost-projection.ts +12 -17
  58. package/src/types.ts +29 -7
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  **DSH Cost Meter** is a DeepSeek conversation cost-tracking plugin for the DeepSeek Harness (DSH) Web GUI — **price-snapshot-anchored per-turn cost** (peak/off-peak aware), **account balance**, a **cost view tab**, **per-message cost chips**, and a **header pill with live streaming estimates**. Every step's cost, price band, and snapshot version are computed *once* from the pricebook snapshot effective at the usage event's own time and then never recomputed — a later price change never rewrites an already-written conversation row.
10
10
 
11
- - Host half (`src/`): DeepSeek `GET /user/balance` query, the persisted snapshot-anchored pricebook, the `sessionCost` projection, subagent cost aggregation, and the trust-fenced `/cost-meter` route.
11
+ - Host half (`src/`): DeepSeek `GET /user/balance` query, the persisted snapshot-anchored pricebook, the `sessionCost` projection plus the pricebook-free `sessionCostIndex` message index, subagent cost aggregation, and the trust-fenced `/cost-meter` route.
12
12
  - Client half (`src/client/`): composer-dock readout, Cost tab, per-reply chip, header pill, and the plugin configuration card — in Simplified Chinese and English.
13
13
 
14
14
  ---
@@ -29,11 +29,11 @@ Then start the harness:
29
29
  npx @deepseek-ai/dsh web
30
30
  ```
31
31
 
32
- If you have the DSH CLI installed globally, you can also use `dsh` instead of `npx @deepseek-ai/dsh`. To install into another profile, replace `web` with your profile name. The plugin declares and is verified against DSH `>=0.1.1-rc.1 <0.2.0` (current local version `0.1.1-rc.1`). The host half requires Node `^22.19.0 || >=24.0.0` and pnpm `11.7.0` for development.
32
+ If you have the DSH CLI installed globally, you can also use `dsh` instead of `npx @deepseek-ai/dsh`. To install into another profile, replace `web` with your profile name. The plugin declares and is verified against DSH `^0.1.5-alpha.1`. The host half requires Node `^22.19.0 || >=24.0.0` and pnpm `11.7.0` for development.
33
33
 
34
34
  ## Overview
35
35
 
36
- Chat costs in DeepSeek pricing change over time (list prices, USD→CNY exchange, and the 2026-08-17 peak/off-peak rollout; on 2026-08-21 the official zh/en pages were redesigned into a combined table with three model columns and OFF-PEAK/PEAK cells), and a conversation spans many turns with cache-hit, cache-miss, cache-write, and output token buckets. Naively recomputing costs at *current* prices makes history drift every time the price table changes.
36
+ Chat costs in DeepSeek pricing change over time (list prices, USD→CNY exchange, the 2026-08-17 peak/off-peak rollout, and the 2026-09-10 model rename to `deepseek-flash` with its V4.1-Flash price cut), and a conversation spans many turns with cache-hit, cache-miss, cache-write, and output token buckets. Naively recomputing costs at *current* prices makes history drift every time the price table changes.
37
37
 
38
38
  **Cost Meter** solves this with an **append-only pricebook**: every price/fx/band-table change starts a new immutable `PricebookSnapshot` (monotonic `version`, `effectiveAt`), and each usage event anchors to the snapshot effective at its own time. The result is an immutable per-step cost ledger that only grows — it never mutates. Live streaming estimates are explicitly labeled 估算/estimate because they use *current* prices; they are replaced by the exact anchored value once the step settles.
39
39
 
@@ -43,8 +43,8 @@ Chat costs in DeepSeek pricing change over time (list prices, USD→CNY exchange
43
43
  |---|---|
44
44
  | Cost anchoring | Append-only pricebook snapshots; step cost computed once at the event's own time |
45
45
  | Price sources | Manual override > official pricing page > built-in fallback > OpenRouter (fallback only, USD→CNY) > none |
46
- | Official page | Parses both the 2026-08-21 combined zh/en tables (including the `deepseek-v4-flash-vision-exp` column and the English UTC schedule) and the legacy split tables; the built-in historical list still anchors `single` when the page no longer carries one |
47
- | Peak pricing | 2026-08-17 00:00 Beijing rollout; peak windows apply Monday–Friday only (the zh and en pages may state different timezones; fallback 09:00–12:00 / 14:00–18:00 Beijing), weekends and all other hours off-peak at half price |
46
+ | Official page | Parses the current 2026-09-10 combined zh/en tables (the `deepseek-flash` / `deepseek-v4-pro` columns with OFF-PEAK/PEAK cells per bucket row, and the English UTC schedule) as well as the earlier combined and legacy split tables; the retired `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` ids have no column of their own and bill at the Flash price, exactly as the page states. The built-in list still anchors `single` when the page no longer carries a pre-rollout table |
47
+ | Peak pricing | 2026-08-17 00:00 Beijing rollout; peak windows apply Monday–Friday only (the zh and en pages may state different timezones; fallback 09:00–12:00 / 14:00–18:00 Beijing), so Saturdays, Sundays, and all other hours are off-peak at half price |
48
48
  | Cost formula | Uncached input + cache reads (hit rate) + cache writes (billed at uncached input rate) + output, per 1M tokens, CNY |
49
49
  | Account balance | Official `GET /user/balance`, cached 60 s, single in-flight request, trust-fenced route |
50
50
  | Subagent support | BFS over the live agent tree; conversation totals = main session + descendants |
@@ -60,7 +60,7 @@ Once installed, the plugin contributes five browser surfaces (all text shown in
60
60
  |---|---|---|
61
61
  | Composer dock readout | `conversation.composer.dock` | Anchored session spend + account balance, refreshed every minute; hover for the category breakdown and snapshot info |
62
62
  | Cost view tab | `conversation.view` | Whole-conversation totals (main + subagents), category totals, per-subagent list, and the per-reply anchored ledger |
63
- | Per-reply cost chip | `conversation.chat.assistant-actions` | The anchored cost of one finalized reply (dash `—` when unpriced) |
63
+ | Per-reply cost chip | `conversation.chat.assistant-actions` | The anchored cost of one finalized reply as the row's own stat capsule — the same trigger + anchored dialog the shipped 用量 and 用时 capsules use (28px pill, hover fill, `aria-expanded` dialog above the trigger). It closes the row's stat run: after 用量 and 用时, before the timestamp, spaced by the row's own gap, and coloured by the band that priced it (green off-peak, red peak). The dialog lists the three billed categories, the band with its multiplier, the model, and the snapshot version; the slot hands over the message id, which the `sessionCostIndex` projection resolves to ledger coordinates (dash `—` when unpriced) |
64
64
  | Header pill | `conversation.session.header.utilities` | Anchored total, or a live `≈ ¥x.xx (estimate)` while streaming; click for the detail panel |
65
65
  | Plugin card | `settings.plugin.item` | Per-model overrides, OpenRouter aliases, cache-read discount, FX mode, toggles, and manual refresh |
66
66
 
@@ -73,7 +73,7 @@ The pricebook (`src/pricebook.ts`) is the durable price source, persisted on the
73
73
  - **Priority chain** — per canonical model key (`provider/model`, bare model, or the `flash`/`pro` pricing key for DeepSeek-family models): manual override > official page > built-in fallback > OpenRouter (fallback only, USD→CNY, cache reads at the configured discount) > none.
74
74
  - **Snapshot selection** — `snapshotForTime` picks the newest snapshot with `effectiveAt <= event time` (pre-install sessions anchor to the first snapshot once).
75
75
  - **Peak/off-peak** — before the 2026-08-17 rollout all steps price at the single list price; after it, the band is chosen by the EVENT's own time against the schedule of the pricebook's own page (the zh and en pages each parse their own peak windows — the redesigned English page states UTC — falling back to Beijing 09:00–12:00 / 14:00–18:00, everything else off-peak). The official pages restrict peak windows to Monday–Friday, so Saturdays and Sundays are off-peak all day in the schedule's own timezone. When the combined page has no legacy single-price column, `single` keeps anchoring to the built-in historical list; a page that still carries a separate legacy table wins. Each step's band is anchored once at fold time, so the per-reply cards and chips always show the band that round was billed at — never the band of the moment you are looking.
76
- - **Immutable ledger** — the `sessionCost` projection (`src/session-cost-projection.ts`) folds `request/header` (model) and usage-carrying events into per-step rows; a second usage sample for the same (turn, step) replaces the first (same-step finalization, not a re-price), with O(1) incremental totals.
76
+ - **Immutable ledger** — the `sessionCost` projection (`src/session-cost-projection.ts`) folds `request/header` (model) and usage-carrying events into per-step rows; a second usage sample for the same (turn, step) replaces the first (same-step finalization, not a re-price), with O(1) incremental totals. The companion `sessionCostIndex` projection (`src/session-cost-index.ts`) maps each finalized assistant-message id to its ledger coordinates; it reads no pricebook and computes no cost, so folding it can never re-price a session.
77
77
 
78
78
  ## Project Structure
79
79
 
@@ -84,6 +84,7 @@ src/
84
84
  pricing.ts # Official pricing-page parser, peak pricing, Beijing bands
85
85
  pricebook.ts # Append-only snapshots, priority chain, storage domain
86
86
  session-cost-projection.ts # sessionCost projection (immutable per-step ledger)
87
+ session-cost-index.ts # sessionCostIndex: message id → ledger coordinates
87
88
  subagent-cost.ts # BFS subagent cost aggregation
88
89
  invariant.ts # Route-disposer symmetry invariant companion
89
90
  client/ # Browser half: 5 slot components + math/format/locales
@@ -107,7 +108,7 @@ The test suites are fully offline: pricing-page HTML, OpenRouter models, and the
107
108
 
108
109
  ## Documentation
109
110
 
110
- - [`src/pricing.ts`](src/pricing.ts), [`src/pricebook.ts`](src/pricebook.ts), [`src/session-cost-projection.ts`](src/session-cost-projection.ts) — detailed module docs on parsing, anchoring, and the ledger contract
111
+ - [`src/pricing.ts`](src/pricing.ts), [`src/pricebook.ts`](src/pricebook.ts), [`src/session-cost-projection.ts`](src/session-cost-projection.ts), [`src/session-cost-index.ts`](src/session-cost-index.ts) — detailed module docs on parsing, anchoring, and the ledger contract
111
112
  - [`README.zh-CN.md`](README.zh-CN.md) — 简体中文版本
112
113
 
113
114
  ## License
package/README.zh-CN.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  **DSH Cost Meter** 是为 DeepSeek Harness(DSH)Web GUI 打造的对话成本追踪插件——**价格快照锚定**的逐轮成本(感知**峰值/闲时**)、**账户余额**、**花费标签页**、**每条回复成本小标签**,以及带**流式实时估算**的**头部胶囊**。每一步的成本、价格档位与快照版本都**只计算一次**——锚定到该用量事件自身时刻生效的价格快照,此后永不重算——因此后续价格变动绝不会改写已写入的对话记录。
10
10
 
11
- - Host 半区(`src/`):DeepSeek `GET /user/balance` 余额查询、持久化的快照锚定价格簿、`sessionCost` 投影、子代理成本聚合,以及带信任围栏的 `/cost-meter` 路由。
11
+ - Host 半区(`src/`):DeepSeek `GET /user/balance` 余额查询、持久化的快照锚定价格簿、`sessionCost` 投影与不读价格簿的 `sessionCostIndex` 消息索引、子代理成本聚合,以及带信任围栏的 `/cost-meter` 路由。
12
12
  - Client 半区(`src/client/`):输入框下方读数、花费标签页、每条回复成本小标签、头部胶囊,以及插件配置卡——内置简体中文与英文。
13
13
 
14
14
  ---
@@ -29,11 +29,11 @@ npx @deepseek-ai/dsh plugin --profile web add @gamegeek-saikel/dsh-cost-meter
29
29
  npx @deepseek-ai/dsh web
30
30
  ```
31
31
 
32
- 如果已全局安装 DSH CLI,也可以使用 `dsh` 代替 `npx @deepseek-ai/dsh`。安装到其他 profile 时,把 `web` 替换成你的 profile 名称即可。插件声明并已验证兼容 DSH `>=0.1.1-rc.1 <0.2.0`(当前本地版本 `0.1.1-rc.1`)。开发环境要求 Node `^22.19.0 || >=24.0.0` 与 pnpm `11.7.0`。
32
+ 如果已全局安装 DSH CLI,也可以使用 `dsh` 代替 `npx @deepseek-ai/dsh`。安装到其他 profile 时,把 `web` 替换成你的 profile 名称即可。插件声明并已验证兼容 DSH `^0.1.5-alpha.1`。开发环境要求 Node `^22.19.0 || >=24.0.0` 与 pnpm `11.7.0`。
33
33
 
34
34
  ## 概述
35
35
 
36
- DeepSeek 的价格随时间变化(官方价目表、USD→CNY 汇率、以及 2026-08-17 上线的峰值/闲时分时计价;2026-08-21 官方又把中英文页面改为一张三模型列、空闲/高峰列的价格表),而一次对话跨越很多轮,且每轮都含缓存命中、缓存未命中、缓存写入与输出等 token 桶。若按"当前价格"重算成本,每次价目变动都会让历史记录漂移。
36
+ DeepSeek 的价格随时间变化(官方价目表、USD→CNY 汇率、2026-08-17 上线的峰值/闲时分时计价,以及 2026-09-10 的模型改名——Flash 列更名为 `deepseek-flash` 并下调为 V4.1-Flash 价),而一次对话跨越很多轮,且每轮都含缓存命中、缓存未命中、缓存写入与输出等 token 桶。若按"当前价格"重算成本,每次价目变动都会让历史记录漂移。
37
37
 
38
38
  **Cost Meter** 用**只追加的价格簿**解决这一问题:每次价格/汇率/分时表变化都会开启一个新的不可变 `PricebookSnapshot`(单调 `version`、`effectiveAt`),每个用量事件锚定到其自身时刻生效的快照。结果是一个**只增长、永不改写**的不可变逐步成本账本。流式实时估算明确标注为"估算"——因为它使用*当前*价格;一旦该步结算,即被精确的锚定值取代。
39
39
 
@@ -43,8 +43,8 @@ DeepSeek 的价格随时间变化(官方价目表、USD→CNY 汇率、以及
43
43
  |---|---|
44
44
  | 成本锚定 | 只追加价格簿快照;步成本在事件自身时刻只计算一次 |
45
45
  | 价格来源 | 手动覆盖 > 官方价格页 > 内置回退 > OpenRouter(仅回退,USD→CNY)> 无 |
46
- | 官方页适配 | 同时解析 2026-08-21 新版中英文页面(含 `deepseek-v4-flash-vision-exp` 第三列与英文 UTC 时段)和旧版分表;无旧单价的页面继续用内置历史单价锚定 |
47
- | 峰值定价 | 2026-08-17 00:00 北京生效;高峰窗口仅周一至周五(中英文页面时区可能不同,缺省 09:00–12:00 / 14:00–18:00 北京),周六、周日全天及其余时段为半价闲时 |
46
+ | 官方页适配 | 解析当前 2026-09-10 版中英文页面(`deepseek-flash` / `deepseek-v4-pro` 两列、每个 token 桶行内的空闲/高峰单元格,以及英文 UTC 时段),同时兼容此前的合并表与旧版分表;已下线的 `deepseek-v4-flash`、`deepseek-v4-flash-vision-exp` 不再有独立列,按页面说明直接以 Flash 价格计费;页面不再提供旧单价表时继续用内置历史单价锚定 `single` |
47
+ | 峰值定价 | 2026-08-17 00:00 北京生效;高峰窗口仅周一至周五(中英文页面时区可能不同,缺省 09:00–12:00 / 14:00–18:00 北京),因此周六、周日全天及其余时段均为半价闲时 |
48
48
  | 成本公式 | 未命中输入 + 缓存命中(命中价)+ 缓存写入(按未命中输入价计)+ 输出,每百万 tokens,CNY |
49
49
  | 账户余额 | 官方 `GET /user/balance`,缓存 60 秒,单飞请求,路由带信任围栏 |
50
50
  | 子代理支持 | 沿活跃代理树 BFS;对话总花费 = 主会话 + 全部后代 |
@@ -60,7 +60,7 @@ DeepSeek 的价格随时间变化(官方价目表、USD→CNY 汇率、以及
60
60
  |---|---|---|
61
61
  | 输入框下方读数 | `conversation.composer.dock` | 锚定的本会话花费 + 账户余额,每分钟刷新;悬停查看分类明细与快照信息 |
62
62
  | 花费标签页 | `conversation.view` | 全对话总花费(主会话 + 子代理)、分类小计、子代理列表与逐回复锚定账本 |
63
- | 每条回复成本小标签 | `conversation.chat.assistant-actions` | 单条已定稿回复的锚定成本(无价格时显示 `—`) |
63
+ | 每条回复成本小标签 | `conversation.chat.assistant-actions` | 单条已定稿回复的锚定成本,做成与官方「用量」「用时」完全同形的统计胶囊(28px 药丸、悬停底色、`aria-expanded` 展开锚定在触发件上方的对话框);位置收在统计串末:在「用量」「用时」之后、末尾时刻文本之前,间距沿用行自身的 gap;按计价档位着色(闲时绿、高峰红);对话框列出三类计费、档位与倍率、模型与快照版本;插槽只给出消息 id,由 `sessionCostIndex` 投影解析为账本坐标(无价格时显示 `—`) |
64
64
  | 头部胶囊 | `conversation.session.header.utilities` | 锚定总花费;流式中显示 `预计 ¥x.xx(估算)`;点击展开详情面板 |
65
65
  | 插件配置卡 | `settings.plugin.item` | 按模型覆盖价、OpenRouter 别名、缓存折扣、汇率模式、开关与立即刷新 |
66
66
 
@@ -73,7 +73,7 @@ DeepSeek 的价格随时间变化(官方价目表、USD→CNY 汇率、以及
73
73
  - **优先级链**——按规范模型键(`provider/model`、裸模型名,或 DeepSeek 系模型的 `flash`/`pro` 定价键):手动覆盖 > 官方页面 > 内置回退 > OpenRouter(仅回退,USD→CNY,缓存读按配置折扣)> 无。
74
74
  - **快照选取**——`snapshotForTime` 取 `effectiveAt <= 事件时间` 的最新快照(安装前的会话一次性锚到首个快照)。
75
75
  - **峰值/闲时**——2026-08-17 上线前所有步按单一价目计费;上线后按事件自身时刻与该价格簿页面的时段表选档(中英文页面各自解析自己的高峰窗口,英文新版为 UTC,抓取失败时回退到北京 09:00–12:00 / 14:00–18:00,其余闲时)。官方页面限定高峰仅周一至周五,因此北京时间周六、周日全天为闲时。新版合并表没有旧单价列时,`single` 继续锚定内置历史单价;页面若有独立旧表,则优先用它。每步的档位在折叠时锚定一次,逐回复卡片与回复小标签永远显示该轮对话当时被计价的档位,而不是查看时刻的档位。
76
- - **不可变账本**——`sessionCost` 投影(`src/session-cost-projection.ts`)把 `request/header`(模型)与携带用量的事件折叠为逐步记录;同一 (turn, step) 的第二次用量样本**替换**第一条(同一步终结,而非重新计价),总计以 O(1) 增量维护。
76
+ - **不可变账本**——`sessionCost` 投影(`src/session-cost-projection.ts`)把 `request/header`(模型)与携带用量的事件折叠为逐步记录;同一 (turn, step) 的第二次用量样本**替换**第一条(同一步终结,而非重新计价),总计以 O(1) 增量维护。配套的 `sessionCostIndex` 投影(`src/session-cost-index.ts`)把每条已定稿助手消息 id 映射到其账本坐标;它不读价格簿、不算成本,因此折叠它绝不会重新计价任何会话。
77
77
 
78
78
  ## 项目结构
79
79
 
@@ -84,6 +84,7 @@ src/
84
84
  pricing.ts # 官方价格页解析、峰值定价、北京时段
85
85
  pricebook.ts # 只追加快照、优先级链、存储域
86
86
  session-cost-projection.ts # sessionCost 投影(不可变逐步账本)
87
+ session-cost-index.ts # sessionCostIndex:消息 id → 账本坐标
87
88
  subagent-cost.ts # BFS 子代理成本聚合
88
89
  invariant.ts # 路由释放对称性 invariant 伴生插件
89
90
  client/ # 浏览器半区:5 个插槽组件 + 数学/格式化/本地化
@@ -107,7 +108,7 @@ pnpm build # tsc -b && tsdown(lib/ + lib/client.js)
107
108
 
108
109
  ## 文档
109
110
 
110
- - [`src/pricing.ts`](src/pricing.ts)、[`src/pricebook.ts`](src/pricebook.ts)、[`src/session-cost-projection.ts`](src/session-cost-projection.ts)——解析、锚定与账本契约的详细模块注释
111
+ - [`src/pricing.ts`](src/pricing.ts)、[`src/pricebook.ts`](src/pricebook.ts)、[`src/session-cost-projection.ts`](src/session-cost-projection.ts)、[`src/session-cost-index.ts`](src/session-cost-index.ts)——解析、锚定与账本契约的详细模块注释
111
112
  - [`README.md`](README.md) — English version
112
113
 
113
114
  ## 许可证
package/lib/client.js CHANGED
@@ -7,6 +7,7 @@ window.__ModuleLoader__.load({
7
7
  let react = require("react");
8
8
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
+ let react_dom = require("react-dom");
10
11
  //#region src/client/locales.ts
11
12
  /** `cost-meter` namespace dictionaries. */
12
13
  /** Dictionary namespace owned by this plugin. */
@@ -56,6 +57,10 @@ window.__ModuleLoader__.load({
56
57
  "price.peakRatio": "高峰 {multiplier}",
57
58
  "price.offPeakRatio": "闲时 {multiplier}",
58
59
  "chip.title": "本回复花费 {amount}",
60
+ "chip.detail.title": "本回复花费",
61
+ "chip.detail.band": "计价档位",
62
+ "chip.detail.model": "模型",
63
+ "chip.detail.snapshot": "价格快照",
59
64
  "chip.unpriced": "—",
60
65
  "balance.label": "余额 {amount}",
61
66
  "balance.failed": "余额不可用",
@@ -126,6 +131,10 @@ window.__ModuleLoader__.load({
126
131
  "price.peakRatio": "peak {multiplier}",
127
132
  "price.offPeakRatio": "off-peak {multiplier}",
128
133
  "chip.title": "This reply cost {amount}",
134
+ "chip.detail.title": "Cost of this reply",
135
+ "chip.detail.band": "Price band",
136
+ "chip.detail.model": "Model",
137
+ "chip.detail.snapshot": "Price snapshot",
129
138
  "chip.unpriced": "—",
130
139
  "balance.label": "Balance {amount}",
131
140
  "balance.failed": "Balance unavailable",
@@ -156,17 +165,16 @@ window.__ModuleLoader__.load({
156
165
  /** Official peak-pricing rollout: 2026-08-17 00:00 Beijing time (UTC+8). */
157
166
  const PEAK_PRICING_START_MS = Date.UTC(2026, 7, 16, 16, 0, 0);
158
167
  /**
159
- * Built-in fallback zh peak schedule: the official announcement's windows in
160
- * Beijing time (peak 09:00-12:00 / 14:00-18:00, Monday–Friday; everything
161
- * else, including all of Saturday and Sunday, off-peak).
168
+ * Built-in fallback zh peak schedule: the official page's windows in Beijing
169
+ * time (peak 09:00-12:00 / 14:00-18:00, Monday–Friday; everything else,
170
+ * including all of Saturday and Sunday, off-peak).
162
171
  */
163
172
  const PEAK_SCHEDULE_ZH = {
164
173
  timezone: "Asia/Shanghai",
165
174
  ranges: [[9, 12], [14, 18]],
166
175
  weekdaysOnly: true
167
176
  };
168
- /** Built-in fallback USD prices from the English pricing page. */
169
- const FALLBACK_CURRENT_USD = {
177
+ ({ ...{
170
178
  flash: {
171
179
  cacheReadPerMillion: .007,
172
180
  inputPerMillion: .22,
@@ -177,8 +185,7 @@ window.__ModuleLoader__.load({
177
185
  inputPerMillion: .66,
178
186
  outputPerMillion: 1.98
179
187
  }
180
- };
181
- ({ ...FALLBACK_CURRENT_USD.flash }), { ...FALLBACK_CURRENT_USD.pro };
188
+ }.pro });
182
189
  /**
183
190
  * Whether the given instant is a peak-pricing hour under one schedule: the
184
191
  * zh fallback is Beijing time 09:00-12:00 and 14:00-18:00; each pricebook
@@ -323,28 +330,6 @@ window.__ModuleLoader__.load({
323
330
  if (offPeakTotal === 0) return null;
324
331
  return (peak.cacheReadPerMillion + peak.inputPerMillion + peak.outputPerMillion) / offPeakTotal;
325
332
  }
326
- /** CJK characters (the /1.5 density class). */
327
- const CJK_RE = /[\u3400-\u4dbf\u4e00-\u9fff]/g;
328
- /**
329
- * Heuristic character→token estimate: CJK ÷ 1.5, everything else ÷ 4
330
- * (the streaming estimate's output side).
331
- * @param text - the accumulated text.
332
- * @returns the estimated token count.
333
- */
334
- function estimateTokens(text) {
335
- const cjk = (text.match(CJK_RE) ?? []).length;
336
- return Math.ceil(cjk / 1.5 + (text.length - cjk) / 4);
337
- }
338
- /**
339
- * Tokens of the streamed text/reasoning blocks of a partial assistant.
340
- * @param blocks - the partial's content blocks (text/reasoning carry `text`).
341
- * @returns the estimated token count.
342
- */
343
- function blocksOutputTokens(blocks) {
344
- let tokens = 0;
345
- for (const block of blocks ?? []) if ((block.kind === "text" || block.kind === "reasoning") && typeof block.text === "string" && block.text.length > 0) tokens += estimateTokens(block.text);
346
- return tokens;
347
- }
348
333
  /**
349
334
  * The cache-read ratio of the newest completed step (input side of the
350
335
  * estimate). Absent history conservatively prices all input as uncached
@@ -585,7 +570,7 @@ window.__ModuleLoader__.load({
585
570
  });
586
571
  //#endregion
587
572
  //#region \0dsh-css:/home/runner/work/dsh-cost-meter/dsh-cost-meter/src/client/CostView.module.css.mjs
588
- const css$3 = ".fZF-xq_root{max-width:var(--dsh-chat-content-width);box-sizing:border-box;width:100%;color:var(--dsw-alias-label-primary);flex-direction:column;gap:16px;margin:0 auto;padding:16px 24px;font-size:13px;line-height:20px;display:flex}.fZF-xq_summary{flex-direction:column;gap:8px;display:flex}.fZF-xq_totalRow{justify-content:space-between;align-items:baseline;display:flex}.fZF-xq_totalLabel{font-size:15px;font-weight:600}.fZF-xq_totalValue{font-variant-numeric:tabular-nums;font-size:18px;font-weight:700}.fZF-xq_bandBadge{opacity:.85;white-space:nowrap;font-size:11px;font-weight:500}.fZF-xq_bandBadge[data-band=peak]{color:var(--dsw-alias-state-error-primary)}.fZF-xq_bandBadge[data-band=offPeak]{color:var(--dsw-alias-state-success-primary)}.fZF-xq_bandBadge:before{content:\"·\";color:currentColor;opacity:.6;margin:0 4px}.fZF-xq_categories{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex-wrap:wrap;gap:4px 16px;font-size:12px;display:flex}.fZF-xq_unpricedNote{color:var(--dsw-alias-label-secondary);opacity:.75;font-size:12px}.fZF-xq_steps{flex-direction:column;gap:8px;display:flex}.fZF-xq_stepsTitle{margin:0;font-size:13px;font-weight:600}.fZF-xq_stepList{flex-direction:column;gap:6px;margin:0;padding:0;list-style:none;display:flex}.fZF-xq_step{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:8px;flex-direction:column;gap:2px;padding:8px 10px;display:flex}.fZF-xq_step[data-band=peak]{border-color:var(--dsw-alias-state-error-secondary)}.fZF-xq_step[data-band=offPeak]{border-color:var(--dsw-alias-state-success-secondary)}.fZF-xq_step[data-band=peak] .fZF-xq_stepCost{color:var(--dsw-alias-state-error-primary)}.fZF-xq_step[data-band=offPeak] .fZF-xq_stepCost{color:var(--dsw-alias-state-success-primary)}.fZF-xq_stepHead{flex-wrap:wrap;align-items:baseline;gap:8px;display:flex}.fZF-xq_stepTurn{font-weight:600}.fZF-xq_stepModel{color:var(--dsw-alias-label-secondary);font-size:12px}.fZF-xq_stepTime{color:var(--dsw-alias-label-secondary);opacity:.75;font-size:12px}.fZF-xq_stepCost{font-variant-numeric:tabular-nums;align-items:baseline;gap:4px;margin-left:auto;font-weight:600;display:inline-flex}.fZF-xq_stepBody{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex-wrap:wrap;gap:12px;font-size:12px;display:flex}.fZF-xq_stepMeta{color:var(--dsw-alias-label-secondary);opacity:.75}";
573
+ const css$3 = ".fZF-xq_root{max-width:var(--dsh-chat-content-width);box-sizing:border-box;width:100%;color:var(--dsw-alias-label-primary);flex-direction:column;gap:16px;margin:0 auto;padding:16px 24px;font-size:13px;line-height:20px;display:flex}.fZF-xq_summary{flex-direction:column;gap:8px;display:flex}.fZF-xq_totalRow{justify-content:space-between;align-items:baseline;display:flex}.fZF-xq_totalLabel{font-size:15px;font-weight:600}.fZF-xq_totalValue{font-variant-numeric:tabular-nums;font-size:18px;font-weight:700}.fZF-xq_categories{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex-wrap:wrap;gap:4px 16px;font-size:12px;display:flex}.fZF-xq_unpricedNote{color:var(--dsw-alias-label-secondary);opacity:.75;font-size:12px}.fZF-xq_steps{flex-direction:column;gap:8px;display:flex}.fZF-xq_stepsTitle{margin:0;font-size:13px;font-weight:600}.fZF-xq_stepList{flex-direction:column;gap:6px;margin:0;padding:0;list-style:none;display:flex}.fZF-xq_step{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:8px;flex-direction:column;gap:2px;padding:8px 10px;display:flex}.fZF-xq_step[data-band=peak]{border-color:var(--dsw-alias-state-error-secondary)}.fZF-xq_step[data-band=offPeak]{border-color:var(--dsw-alias-state-success-secondary)}.fZF-xq_step[data-band=peak] .fZF-xq_stepCost{color:var(--dsw-alias-state-error-primary)}.fZF-xq_step[data-band=offPeak] .fZF-xq_stepCost{color:var(--dsw-alias-state-success-primary)}.fZF-xq_stepHead{flex-wrap:wrap;align-items:baseline;gap:8px;display:flex}.fZF-xq_stepTurn{font-weight:600}.fZF-xq_stepModel{color:var(--dsw-alias-label-secondary);font-size:12px}.fZF-xq_stepTime{color:var(--dsw-alias-label-secondary);opacity:.75;font-size:12px}.fZF-xq_stepCost{font-variant-numeric:tabular-nums;align-items:baseline;gap:6px;margin-left:auto;font-weight:600;display:inline-flex}.fZF-xq_stepBody{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex-wrap:wrap;gap:12px;font-size:12px;display:flex}.fZF-xq_stepMeta{color:var(--dsw-alias-label-secondary);opacity:.75}";
589
574
  const tagId$3 = "@gamegeek-saikel/dsh-cost-meter/CostView.module.css";
590
575
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
591
576
  const tag = document.createElement("style");
@@ -595,7 +580,6 @@ window.__ModuleLoader__.load({
595
580
  document.head.appendChild(tag);
596
581
  }
597
582
  var CostView_module_css_default = {
598
- "bandBadge": "fZF-xq_bandBadge",
599
583
  "categories": "fZF-xq_categories",
600
584
  "root": "fZF-xq_root",
601
585
  "step": "fZF-xq_step",
@@ -624,7 +608,9 @@ window.__ModuleLoader__.load({
624
608
  * per-subagent breakdown, and the per-reply list (turn, time, model, token
625
609
  * split, step cost, anchored snapshot version and effective time). Reads the
626
610
  * `sessionCost` projection plus the host-aggregated subagent totals; nothing
627
- * is recomputed at current prices.
611
+ * is recomputed at current prices. Each row's band rides the shipped `Tag`
612
+ * capsule (11px/17px, `success` off-peak / `danger` peak), the same tag
613
+ * geometry every other tag control in the product uses.
628
614
  */
629
615
  /** Map an unpriced reason to its localized copy. */
630
616
  function reasonText(step, t) {
@@ -771,10 +757,8 @@ window.__ModuleLoader__.load({
771
757
  }),
772
758
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
773
759
  className: CostView_module_css_default.stepCost,
774
- children: [money(step.cost), stepBandLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
775
- className: CostView_module_css_default.bandBadge,
776
- "data-band": stepBand,
777
- "data-testid": "cost-step-band",
760
+ children: [money(step.cost), stepBandLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
761
+ tone: stepBand === "peak" ? "danger" : "success",
778
762
  children: stepBandLabel
779
763
  })]
780
764
  })
@@ -802,7 +786,7 @@ window.__ModuleLoader__.load({
802
786
  });
803
787
  //#endregion
804
788
  //#region \0dsh-css:/home/runner/work/dsh-cost-meter/dsh-cost-meter/src/client/AssistantCostChip.module.css.mjs
805
- const css$2 = "._9lx30q_root{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-variant-numeric:tabular-nums;order:10;align-items:baseline;gap:4px;margin-left:10px;font-size:14px;line-height:24px;display:inline-flex}._9lx30q_root:before{content:\"·\";color:var(--dsw-alias-separator-primary);margin-right:10px}._9lx30q_bandBadge{opacity:.85;white-space:nowrap;font-size:12px;line-height:18px}._9lx30q_bandBadge[data-band=peak]{color:var(--dsw-alias-state-error-primary)}._9lx30q_bandBadge[data-band=offPeak]{color:var(--dsw-alias-state-success-primary)}._9lx30q_bandBadge:before{content:\"·\";color:currentColor;opacity:.6;margin:0 4px}._9lx30q_unpriced{color:var(--dsw-alias-label-tertiary);white-space:nowrap;opacity:.6;cursor:help;order:10;margin-left:10px;font-size:14px;line-height:24px}._9lx30q_unpriced:before{content:\"·\";color:var(--dsw-alias-separator-primary);margin-right:10px}@media (hover:hover){[data-time-hover-root] [data-cost-chip]{opacity:0;transition:opacity 80ms}[data-time-hover-root]:hover [data-cost-chip],[data-time-hover-root]:focus-within [data-cost-chip]{opacity:1}[data-time-hover-root] [data-cost-chip]._9lx30q_unpriced{opacity:0}}";
789
+ const css$2 = "._9lx30q_root{order:1;min-width:0;display:inline-flex}[class*=_actions]>[class*=_timeEnd]{order:2}._9lx30q_trigger{min-width:0;height:calc(28px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,13px);font-variant-numeric:tabular-nums;line-height:calc(24px + var(--dsh-content-font-delta,0px));white-space:nowrap;cursor:pointer;background:0 0;border:none;border-radius:28px;align-items:center;gap:4px;padding:6px 8px;display:inline-flex}._9lx30q_trigger:hover,._9lx30q_trigger[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}._9lx30q_trigger svg{width:calc(15px + var(--dsh-content-font-delta,0px));height:calc(15px + var(--dsh-content-font-delta,0px));flex:none}._9lx30q_label{text-overflow:ellipsis;min-width:0;overflow:hidden}._9lx30q_root[data-band=offPeak] ._9lx30q_label{color:var(--dsw-alias-state-success-primary)}._9lx30q_root[data-band=peak] ._9lx30q_label{color:var(--dsw-alias-state-error-primary)}._9lx30q_panel{z-index:1100;box-sizing:border-box;background:var(--dsw-specific-menu);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);width:max-content;min-width:min(300px,100vw - 24px);max-width:min(440px,100vw - 24px);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-secondary);cursor:default;border:0;border-radius:12px;padding:16px;font-size:12px;line-height:18px;position:fixed}._9lx30q_title{color:var(--dsw-alias-label-primary);justify-content:space-between;gap:16px;margin-bottom:8px;font-weight:500;display:flex}._9lx30q_titleLabel{align-items:center;gap:6px;min-width:0;display:inline-flex}._9lx30q_titleLabel svg{flex:none;width:14px;height:14px}._9lx30q_titleValue{font-variant-numeric:tabular-nums}._9lx30q_titleRule{border-top:.5px solid var(--dsw-alias-border-l2);margin-bottom:10px}._9lx30q_details{color:var(--dsw-alias-label-tertiary);grid-template-columns:minmax(76px,auto) minmax(0,1fr);gap:6px 16px;margin:0;display:grid}._9lx30q_details dt,._9lx30q_details dd{min-width:0;margin:0}._9lx30q_details dd{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;text-align:right}._9lx30q_unpriced{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));opacity:.6;cursor:help;order:1;align-items:center;display:inline-flex}";
806
790
  const tagId$2 = "@gamegeek-saikel/dsh-cost-meter/AssistantCostChip.module.css";
807
791
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
808
792
  const tag = document.createElement("style");
@@ -812,52 +796,75 @@ window.__ModuleLoader__.load({
812
796
  document.head.appendChild(tag);
813
797
  }
814
798
  var AssistantCostChip_module_css_default = {
815
- "bandBadge": "_9lx30q_bandBadge",
799
+ "details": "_9lx30q_details",
800
+ "label": "_9lx30q_label",
801
+ "panel": "_9lx30q_panel",
816
802
  "root": "_9lx30q_root",
803
+ "title": "_9lx30q_title",
804
+ "titleLabel": "_9lx30q_titleLabel",
805
+ "titleRule": "_9lx30q_titleRule",
806
+ "titleValue": "_9lx30q_titleValue",
807
+ "trigger": "_9lx30q_trigger",
817
808
  "unpriced": "_9lx30q_unpriced"
818
809
  };
819
810
  //#endregion
820
811
  //#region src/client/AssistantCostChip.tsx
821
812
  /**
822
- * Per-reply cost chip: the anchored cost of ONE finalized reply. Rendered
823
- * through the assistant-actions slot but visually joined to the reply's
824
- * timing row (01:51 · 用时 · 首 token · tok/s): a flex `order` places it
825
- * after the clock text, and the type matches the clock's (label-tertiary,
826
- * 14px) with a dot separator, so it reads as part of the same metadata run.
827
- * The `messageId` owner prop maps to the assistant node's (turn, step),
828
- * which addresses the projection's step ledger — the cost is the anchored
829
- * snapshot value, never a current-price recompute. Unpriced replies render
830
- * `—` (the Cost tab explains why).
831
- */
832
- /**
833
- * Locate a finalized assistant node by its message id and return its
834
- * (turn, step) coordinates, or null when the node is out of window.
835
- * @param snapshot - the conversation snapshot.
836
- * @param messageId - the addressed message identity.
837
- * @returns the step coordinates, or null.
813
+ * Per-reply cost, rendered in the completed Turn's action row. Contributed
814
+ * through the `conversation.chat.assistant-actions` list slot, whose entries
815
+ * DSH 0.1.5 draws in the row's action area (after the copy action, ahead of the
816
+ * branch button); a flex `order` then seats the capsule after the shipped 用量
817
+ * and 用时 capsules and before the row's timestamp, closing the stat run with
818
+ * the row's own 8px gaps between neighbours.
819
+ *
820
+ * The control is the row's own stat-capsule shape — the same trigger + portaled
821
+ * dialog the shipped 用量 and 用时 capsules use (28px pill, 13px/24px tertiary
822
+ * label, hover fill, an `aria-expanded` dialog anchored above the trigger
823
+ * through `useAnchoredPosition` + `useDismissOnOutsidePointer`). Clicking it
824
+ * opens the reply's anchored breakdown: the three billed categories, the band
825
+ * that priced it, the model, and the snapshot version.
826
+ *
827
+ * The price itself is the band-coloured label (green off-peak, red peak,
828
+ * neutral before the rollout), so the row carries the status without a second
829
+ * tag capsule. The slot hands over only the finalized message id, so the
830
+ * `sessionCostIndex` projection resolves it to the (turn, step) coordinates the
831
+ * anchored ledger is keyed by; the Turn's own steps are then summed. Every
832
+ * price is the anchored snapshot value, never a current-price recompute; an
833
+ * unpriced Turn renders `—` (the Cost tab explains why).
838
834
  */
839
- function stepOfMessage(snapshot, messageId) {
840
- const nodes = snapshot.chat.legacy.nodes;
841
- for (let i = nodes.length - 1; i >= 0; i -= 1) {
842
- const node = nodes[i];
843
- if (node?.kind === "assistant" && node.messageId === messageId && node.turn !== void 0) return {
844
- turn: node.turn,
845
- step: node.step
846
- };
847
- }
848
- return null;
849
- }
850
- /** The projection step for one (turn, step) coordinate, if the ledger has it. */
851
- function stepOf(steps, turn, step) {
852
- return steps?.find((entry) => entry.turn === turn && entry.step === step);
835
+ /** Distance kept between the trigger and its dialog (the shipped stat panels' own gap). */
836
+ const PANEL_GAP = 8;
837
+ /** Distance kept between the dialog and the viewport edges. */
838
+ const PANEL_MARGIN = 12;
839
+ /** Pre-measurement placement: mounted but invisible, so the first frame can size it. */
840
+ const MEASURE_STYLE = {
841
+ visibility: "hidden",
842
+ left: 0,
843
+ top: 0
844
+ };
845
+ /** The Turn's own ledger steps, in fold order. */
846
+ function stepsOfTurn(steps, turn) {
847
+ return steps?.filter((entry) => entry.turn === turn) ?? [];
853
848
  }
854
- const AssistantCostChip = (0, react.memo)(function AssistantCostChip({ messageId, useSession, useProjection, t }) {
849
+ const AssistantCostChip = (0, react.memo)(function AssistantCostChip({ messageId, useProjection, t }) {
855
850
  const currency = displayCurrency(t);
856
- const step = useSession((snapshot) => stepOfMessage(snapshot, messageId));
857
851
  const costCny = useProjection("sessionCost");
858
852
  const costUsd = useProjection("sessionCostUsd");
859
- const ledger = step === null ? void 0 : stepOf((currency === "USD" ? costUsd : costCny)?.steps, step.turn, step.step);
853
+ const cost = currency === "USD" ? costUsd : costCny;
854
+ const index = useProjection("sessionCostIndex");
860
855
  const [response, setResponse] = (0, react.useState)(null);
856
+ const [open, setOpen] = (0, react.useState)(false);
857
+ const rootRef = (0, react.useRef)(null);
858
+ const panelRef = (0, react.useRef)(null);
859
+ const pos = (0, _deepseek_ai_dsh_client_ui_primitives.useAnchoredPosition)({
860
+ open,
861
+ anchorRef: rootRef,
862
+ panelRef,
863
+ side: "top",
864
+ gap: PANEL_GAP,
865
+ margin: PANEL_MARGIN
866
+ });
867
+ (0, _deepseek_ai_dsh_client_ui_primitives.useDismissOnOutsidePointer)(rootRef, open, setOpen, panelRef);
861
868
  (0, react.useEffect)(() => {
862
869
  let alive = true;
863
870
  fetch(`/cost-meter?currency=${currency}`, { cache: "no-store" }).then((res) => res.ok ? res.json() : null).then((data) => {
@@ -868,9 +875,20 @@ window.__ModuleLoader__.load({
868
875
  alive = false;
869
876
  };
870
877
  }, [currency]);
871
- if (ledger === void 0 || ledger.cost === null) {
872
- if (ledger === void 0) return null;
873
- const reason = ledger.unpricedReason === "NO_MODEL" ? t("reason.NO_MODEL") : t("reason.NO_PRICE");
878
+ (0, react.useEffect)(() => {
879
+ if (!open) return;
880
+ const onKeyDown = (event) => {
881
+ if (event.key === "Escape") setOpen(false);
882
+ };
883
+ document.addEventListener("keydown", onKeyDown);
884
+ return () => document.removeEventListener("keydown", onKeyDown);
885
+ }, [open]);
886
+ const coordinates = index?.steps[messageId];
887
+ const steps = coordinates === void 0 ? [] : stepsOfTurn(cost?.steps, coordinates.turn);
888
+ const priced = steps.filter((entry) => entry.cost !== null);
889
+ if (steps.length === 0) return null;
890
+ if (priced.length === 0) {
891
+ const reason = steps[0]?.unpricedReason === "NO_MODEL" ? t("reason.NO_MODEL") : t("reason.NO_PRICE");
874
892
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
875
893
  className: AssistantCostChip_module_css_default.unpriced,
876
894
  "data-cost-chip": true,
@@ -879,26 +897,78 @@ window.__ModuleLoader__.load({
879
897
  children: t("chip.unpriced")
880
898
  });
881
899
  }
882
- const amount = `${currencySymbol(currency)}${formatMoney(ledger.cost)}`;
883
- const band = ledger.band ?? bandForTime(ledger.time);
884
- const ratio = peakOffPeakMultiplier(response?.pricebook?.current ?? null, ledger.provider, ledger.model);
900
+ const symbol = currencySymbol(currency);
901
+ const money = (value) => `${symbol}${formatMoney(value)}`;
902
+ const amount = money(priced.reduce((sum, entry) => sum + (entry.cost ?? 0), 0));
903
+ const last = priced[priced.length - 1];
904
+ if (last === void 0) return null;
905
+ const band = last.band ?? bandForTime(last.time);
906
+ const ratio = peakOffPeakMultiplier(response?.pricebook?.current ?? null, last.provider, last.model);
885
907
  const bandLabel = band === "peak" ? ratio === null ? t("band.peak") : t("price.peakRatio", { multiplier: formatMultiplier(ratio) }) : band === "offPeak" ? ratio === null ? t("band.offPeak") : t("price.offPeakRatio", { multiplier: formatMultiplier(1 / ratio) }) : null;
886
908
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
887
909
  className: AssistantCostChip_module_css_default.root,
910
+ ref: rootRef,
888
911
  "data-cost-chip": true,
889
- title: t("chip.title", { amount }),
912
+ "data-band": band === "single" ? void 0 : band,
890
913
  "data-testid": "cost-chip",
891
- children: [amount, bandLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
892
- className: AssistantCostChip_module_css_default.bandBadge,
893
- "data-band": band,
894
- "data-testid": "cost-chip-band",
895
- children: bandLabel
896
- })]
914
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
915
+ type: "button",
916
+ className: AssistantCostChip_module_css_default.trigger,
917
+ "aria-haspopup": "dialog",
918
+ "aria-expanded": open,
919
+ onClick: () => setOpen((prev) => !prev),
920
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconGaugeOutline16, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
921
+ className: AssistantCostChip_module_css_default.label,
922
+ children: amount
923
+ })]
924
+ }), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
925
+ ref: panelRef,
926
+ className: AssistantCostChip_module_css_default.panel,
927
+ role: "dialog",
928
+ "aria-label": t("chip.detail.title"),
929
+ style: pos ?? MEASURE_STYLE,
930
+ "data-testid": "cost-chip-detail",
931
+ children: [
932
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
933
+ className: AssistantCostChip_module_css_default.title,
934
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
935
+ className: AssistantCostChip_module_css_default.titleLabel,
936
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconGaugeOutline16, {}), t("chip.detail.title")]
937
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
938
+ className: AssistantCostChip_module_css_default.titleValue,
939
+ children: amount
940
+ })]
941
+ }),
942
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
943
+ className: AssistantCostChip_module_css_default.titleRule,
944
+ "aria-hidden": true
945
+ }),
946
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", {
947
+ className: AssistantCostChip_module_css_default.details,
948
+ children: [
949
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("view.uncached") }),
950
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: money(priced.reduce((sum, entry) => sum + entry.uncachedCost, 0)) }),
951
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("view.cacheRead") }),
952
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: money(priced.reduce((sum, entry) => sum + entry.cacheReadCost, 0)) }),
953
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("view.output") }),
954
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: money(priced.reduce((sum, entry) => sum + entry.outputCost, 0)) }),
955
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("chip.detail.band") }),
956
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: bandLabel ?? t("band.single") }),
957
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("chip.detail.model") }),
958
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: last.model }),
959
+ last.snapshotVersion !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("chip.detail.snapshot") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: t("step.snapshot", {
960
+ version: String(last.snapshotVersion),
961
+ time: formatTime(last.snapshotEffectiveAt ?? last.time)
962
+ }) })] })
963
+ ]
964
+ })
965
+ ]
966
+ }), document.body)]
897
967
  });
898
968
  });
899
969
  //#endregion
900
970
  //#region \0dsh-css:/home/runner/work/dsh-cost-meter/dsh-cost-meter/src/client/SessionCostPill.module.css.mjs
901
- const css$1 = ".jn2k9a_wrap{display:inline-flex;position:relative}.jn2k9a_pill{border:1px solid var(--dsw-alias-border-l2);height:32px;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);font-variant-numeric:tabular-nums;white-space:nowrap;cursor:pointer;user-select:none;background:0 0;border-radius:18px;justify-content:center;align-items:center;gap:4px;padding:6px 12px;font-size:13px;font-weight:400;line-height:20px;transition:background-color .15s,color .15s,border-color .15s;display:inline-flex}.jn2k9a_pill:hover{background:var(--dsw-alias-interactive-bg-hover)}.jn2k9a_pill:active{transform:translateY(.5px)}.jn2k9a_pill[data-running=true]{color:var(--dsw-alias-brand-primary)}.jn2k9a_pill[data-running=true]:hover{background:var(--dsw-alias-interactive-bg-hover)}.jn2k9a_pill[data-band=peak]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-secondary)}.jn2k9a_pill[data-band=offPeak]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-secondary)}.jn2k9a_bandBadge{opacity:.85;white-space:nowrap;font-size:11px;line-height:16px}.jn2k9a_bandBadge:before{content:\"·\";color:currentColor;opacity:.6;margin:0 4px}.jn2k9a_estimateNote{opacity:.75;font-size:11px}.jn2k9a_detail{z-index:100;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu);min-width:250px;max-width:340px;box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;border-radius:12px;flex-direction:column;gap:5px;padding:12px 14px;font-size:12px;line-height:18px;display:flex;position:absolute;top:calc(100% + 8px);right:0}.jn2k9a_detailRow{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.jn2k9a_detailRowSecondary{color:var(--dsw-alias-label-secondary)}.jn2k9a_detailRowMuted{color:var(--dsw-alias-label-secondary);opacity:.75}.jn2k9a_detailTotal{font-size:13px;font-weight:600}";
971
+ const css$1 = ".jn2k9a_wrap{display:inline-flex;position:relative}.jn2k9a_pill{border:.5px solid var(--dsw-alias-border-l4);height:28px;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);font-variant-numeric:tabular-nums;white-space:nowrap;cursor:pointer;user-select:none;background:0 0;border-radius:14px;justify-content:center;align-items:center;gap:5px;padding:5px 7px;font-size:11px;font-weight:400;line-height:16px;transition:background-color .15s,color .15s,border-color .15s;display:inline-flex}.jn2k9a_pill:hover{background:var(--dsw-alias-interactive-bg-hover)}.jn2k9a_pill:active{transform:translateY(.5px)}.jn2k9a_pill[data-running=true]{color:var(--dsw-alias-brand-primary)}.jn2k9a_pill[data-running=true]:hover{background:var(--dsw-alias-interactive-bg-hover)}.jn2k9a_pill[data-band=peak]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-secondary)}.jn2k9a_pill[data-band=offPeak]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-secondary)}.jn2k9a_estimateNote{opacity:.75;font-size:11px}.jn2k9a_detail{z-index:100;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu);min-width:250px;max-width:340px;box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;border-radius:12px;flex-direction:column;gap:5px;padding:12px 14px;font-size:12px;line-height:18px;display:flex;position:absolute;top:calc(100% + 8px);right:0}.jn2k9a_detailRow{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.jn2k9a_detailRowSecondary{color:var(--dsw-alias-label-secondary)}.jn2k9a_detailRowMuted{color:var(--dsw-alias-label-secondary);opacity:.75}.jn2k9a_detailTotal{font-size:13px;font-weight:600}";
902
972
  const tagId$1 = "@gamegeek-saikel/dsh-cost-meter/SessionCostPill.module.css";
903
973
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
904
974
  const tag = document.createElement("style");
@@ -908,7 +978,6 @@ window.__ModuleLoader__.load({
908
978
  document.head.appendChild(tag);
909
979
  }
910
980
  var SessionCostPill_module_css_default = {
911
- "bandBadge": "jn2k9a_bandBadge",
912
981
  "detail": "jn2k9a_detail",
913
982
  "detailRow": "jn2k9a_detailRow",
914
983
  "detailRowMuted": "jn2k9a_detailRowMuted",
@@ -933,8 +1002,12 @@ window.__ModuleLoader__.load({
933
1002
  * projected context size weighted by the newest completed step's cache
934
1003
  * ratio (conservative all-uncached without history), output at the
935
1004
  * character heuristic — both at CURRENT snapshot prices.
1005
+ *
1006
+ * The band rides the shipped `Tag` capsule (11px/17px, `success` off-peak /
1007
+ * `danger` peak) so the header reads the same tag geometry as every other tag
1008
+ * control in the product.
936
1009
  */
937
- /** Read the conversation's running flag (the pill's estimate gate). */
1010
+ /** Read the session's running flag (the pill's estimate gate). */
938
1011
  function runningOf(snapshot) {
939
1012
  return snapshot.running;
940
1013
  }
@@ -970,7 +1043,7 @@ window.__ModuleLoader__.load({
970
1043
  const cost = currency === "USD" ? costUsd : costCny;
971
1044
  const pressure = useProjection("contextPressure");
972
1045
  const running = useSession(runningOf);
973
- const partial = useSession((snapshot) => snapshot.partial);
1046
+ const partial = null;
974
1047
  const [open, setOpen] = (0, react.useState)(false);
975
1048
  const [response, setResponse] = (0, react.useState)(null);
976
1049
  const wrapRef = (0, react.useRef)(null);
@@ -1004,7 +1077,7 @@ window.__ModuleLoader__.load({
1004
1077
  if (!running || projection === void 0) return null;
1005
1078
  const bucket = bucketAt(snapshot ?? void 0, model?.provider, model?.model ?? "unknown", Date.now(), schedule);
1006
1079
  if (bucket === void 0) return null;
1007
- const stepEstimate = estimateCost(bucket, pressure?.projectedTokens ?? pressure?.pressureTokens ?? 0, cacheReadRatioOf(projection.steps), blocksOutputTokens(partial?.blocks));
1080
+ const stepEstimate = estimateCost(bucket, pressure?.projectedTokens ?? pressure?.pressureTokens ?? 0, cacheReadRatioOf(projection.steps), 0);
1008
1081
  return (combined?.cost ?? 0) + stepEstimate;
1009
1082
  }, [
1010
1083
  running,
@@ -1039,9 +1112,8 @@ window.__ModuleLoader__.load({
1039
1112
  "aria-expanded": open,
1040
1113
  children: [
1041
1114
  label,
1042
- bandLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1043
- className: SessionCostPill_module_css_default.bandBadge,
1044
- "data-testid": "cost-pill-band",
1115
+ bandLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1116
+ tone: band === "peak" ? "danger" : "success",
1045
1117
  children: bandLabel
1046
1118
  }),
1047
1119
  estimate !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -1487,7 +1559,7 @@ window.__ModuleLoader__.load({
1487
1559
  }, CostView));
1488
1560
  ctx.slots.inject("conversation.chat.assistant-actions", () => ctx.slots.register({
1489
1561
  name: "conversation.chat.assistant-actions",
1490
- id: "cost",
1562
+ id: "cost-meter",
1491
1563
  order: 20,
1492
1564
  locale: NS
1493
1565
  }, AssistantCostChip));