dsh-consumption-badge 1.1.0 → 1.2.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 (4) hide show
  1. package/README.md +110 -11
  2. package/lib/client.js +2079 -40
  3. package/lib/index.js +48 -15
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -2,22 +2,93 @@
2
2
 
3
3
  A **persistent** DSH (DeepSeek Harness) Web UI plugin that puts a live consumption badge
4
4
  next to every assistant message, and opens a detail panel with the session's spend curve.
5
+ The UI is **bilingual**: it follows the shell language (Chinese or English) and switches live.
5
6
 
6
7
  ```
7
- ⛽ 25.6k | ≈¥0.020 | valley
8
+ ⛽ 25.6k | ≈¥0.020 | Off-peak
8
9
  ```
9
10
 
10
11
  - **Collapsed pill** – session-cumulative tokens, estimated cost (always prefixed `≈`), and the
11
- current peak/valley tier (red `peak`, green `valley`), injected into
12
+ current peak/off-peak tier (red `Peak`, green `Off-peak`), injected into
12
13
  `conversation.chat.assistant-actions` right after the shipped copy/branch controls.
13
14
  - **Detail modal** (centered, blurred backdrop, `Esc` or backdrop-click to close):
14
15
  cumulative tokens with hit/miss/output split, estimated cost, current pricing tier,
15
16
  DeepSeek account balance, and a **per-turn dual-axis line chart**
16
17
  (left axis: tokens, right axis: ¥) with a hover tooltip showing the model,
17
- input-hit / input-miss / output tokens, and the turn's peak/valley flag.
18
+ input-hit / input-miss / output tokens, and the turn's peak/off-peak flag.
19
+ - **Settings page · Usage** (`settings.section`, id `balance-dsh-consumption`, order 26):
20
+ a top row of four cards — **Total tokens** (with hit/miss/output split, all-history totals),
21
+ **Estimated cost**, **Current window** (peak/off-peak) and **Account balance** (reusing the
22
+ badge's cached balance) — then **all-session history** as one line chart, filterable by time range
23
+ (Today / Last 7 days / Last 30 days / Last 12 months), by workspace
24
+ (All workspaces + every registered workspace + Unassigned), and by an explicit
25
+ calendar range that overrides the dropdown. Bucket size follows the range:
26
+ **hourly for today and any single-day calendar range**, daily for multi-day ranges, and
27
+ monthly for the last 12 months. Left axis (tokens) is hidden until you
28
+ toggle it in the legend; the right axis (¥) is always on. Hovering a point shows its
29
+ total, its peak/off-peak split and the models involved; the footer sums the current
30
+ selection. No data in range → a plain "No usage recorded in this range" note instead of an
31
+ empty chart. The cards are **all-history** totals (independent of the filters); the footer is the
32
+ filtered total, so the two numbers differ by design.
33
+ - **Personalization integration** (auto-detected, no configuration): when
34
+ [`dsh-plugin-personalization`](https://www.npmjs.com/package/dsh-plugin-personalization) is
35
+ installed alongside this plugin, a **Usage popup** block is appended to its settings page —
36
+ same `pp-block` / `pp-field` / `pp-modes` layout and the same `Pill` primitive as the shipped
37
+ personalization rows — offering four backgrounds for the consumption modal:
38
+ **Default / Glass / Custom image / Solid color**.
39
+ - **Default** keeps the themed surface; **Glass** is real `backdrop-filter` glass — both the veil
40
+ behind the modal and the panel itself are theme-derived (`--dsw-alias-bg-base`), so light themes
41
+ get a light haze and dark themes a dark one instead of the panel painting a dark slab over the
42
+ page; **Custom image** takes an image URL *or* a local image plus an **Opacity** slider
43
+ (0–100 %, default 85 %) that sets the background layer's own `opacity` — the same semantic as the
44
+ other personalization background rows, so lowering it fades the image over the themed panel
45
+ instead of dimming it; **Solid color** takes a hex colour and derives a light/dark ink palette so
46
+ the cards, chart and tooltips stay readable (the palette also overrides the tooltip surface).
47
+ - A **Glass** switch appears for Custom image and Solid color, so glass composites with them:
48
+ on an image it blurs the image itself (frosted photo), on a solid it makes the colour
49
+ translucent over a blurred page. A **Glass strength** slider (0–48 px, default 22 px) sets the
50
+ blur radius of the panel. The overlay behind the modal is deliberately never blurred.
51
+ - **Clear** wipes the image, the URL field and the file picker in one go. The image row shows a
52
+ **thumbnail** plus the live save state (Saving… / Saved on this device / `error: …`), and
53
+ failures are logged with a `[consumption-badge]` tag instead of failing silently. The picker is a
54
+ button (Choose image… / Change image…), not a raw file input, so saving never leaves a
55
+ misleading "no file chosen" label behind.
56
+ - Local images are **stored on this machine with no size limit**: the picked file is written as a
57
+ Blob into IndexedDB (`dsh-consumption-assets` → `images`), and the preference only keeps a small
58
+ key — so a 20 MB wallpaper no longer blows the `localStorage` quota. On startup the blob is read
59
+ back into a `blob:` URL for the modal; **Clear** deletes the stored record as well.
60
+ If IndexedDB is unavailable the file is read as a data URL instead, and the row says so.
61
+ - The choice is a local preference (`dsh-consumption:modal-style`) and is applied to the open
62
+ modal immediately. Nothing about the conversation or sidebar backgrounds is touched.
63
+ - **Update notice** (same shape as `dsh-plugin-personalization`'s): ~2.5 s after the page loads the
64
+ plugin asks `https://registry.npmjs.org/dsh-consumption-badge/latest` for the newest version
65
+ (24 h cache, 5 s timeout, fails silently). If the installed version is older — and that version
66
+ was not ignored — a non-blocking card appears in the bottom-right corner with
67
+ **current → latest**, the exact `dsh plugin --profile web update dsh-consumption-badge` command,
68
+ and three actions: **Copy update command**, **Ignore this version** (remembered on this machine)
69
+ and **Remind me later** (this page load only). It never blocks the conversation and never covers
70
+ controls.
18
71
 
19
72
  Everything is an **estimate** for reference only — this plugin is not a billing system of record.
20
73
 
74
+ ## Localization
75
+
76
+ The plugin ships **Chinese and English** copy and follows the DSH shell language live — no reload,
77
+ no second settings page.
78
+
79
+ | Aspect | How it works |
80
+ | --- | --- |
81
+ | Source of truth | The client-side `locale` service (`@deepseek-ai/dsh-client-locale`, `ctx.get('locale')`) |
82
+ | Dictionary | `DICT_EN` in `lib/client.js`, keyed by the **Chinese copy itself**; under `zh` no dictionary is needed because the key is the text, and any key missing from `DICT_EN` falls back to that Chinese text (never a blank string) |
83
+ | Live switching | Slot outlets re-render on a locale switch (the shell subscribes to the locale revision), so `tt()` picks up the new language during the next render; the personalization page block renders in its own React root and subscribes to the locale face itself |
84
+ | Slot labels | The action-row label and the settings tab title are **thunks**, resolved by the slot system at read time, so the tab renames itself on a switch |
85
+ | Late service | If the `locale` service is provided after this plugin's `apply()` ran (boot order differs per composition), the first render retries the bind |
86
+ | Missing service | Without the locale service everything falls back to Chinese — the plugin still works standalone |
87
+ | Host errors | `lib/index.js` never returns localized prose: failures carry a language-neutral `code` + `detail` (and an English `error` for logs). The client maps `code` → copy in the active language, and prints the host's `error` only for an unknown code |
88
+
89
+ Adding a language means adding one dictionary next to `DICT_EN`; the lookup order is
90
+ *active locale → key*, so untranslated keys degrade to Chinese rather than breaking.
91
+
21
92
  ## Requirements
22
93
 
23
94
  | Capability | Why |
@@ -73,7 +144,7 @@ deployments where the bundle layer does mount. Keep only **one** of the two acti
73
144
  | Data | Source |
74
145
  | --- | --- |
75
146
  | Per-turn tokens | `assistant/message.usage` events in the session log, folded per turn (`cacheReadTokens` → hit, `inputTokens + cacheWriteTokens` → miss, `outputTokens` → output; multi-step turns are summed) |
76
- | Turn timing | `turn/start` event time (peak/valley is attributed to **request start**) |
147
+ | Turn timing | `turn/start` event time (peak/off-peak is attributed to **request start**) |
77
148
  | Model per turn | `request/context` / `request/header.config`; falls back to the agent default model, then `deepseek-flash` |
78
149
  | Session totals | the shipped `tokenUsage` projection when present, otherwise the folded turns |
79
150
  | Balance | `GET https://api.deepseek.com/user/balance` via curl, 60s cache |
@@ -82,6 +153,14 @@ Turn records are also mirrored into browser `localStorage` under
82
153
  `dsh-consumption:<sessionId>:<turn>` so the chart survives reloads. Persistence writes are
83
154
  scheduled off the render path (`requestIdleCallback`), and the aggregation is O(turns).
84
155
 
156
+ The settings page reads **those same records** (no second store, no new field): workspace
157
+ attribution is resolved at read time from DSH's own accounting ledger
158
+ (`WorkspaceView.sessionIds` from `useWorkspaces`), so historical records are attributed without
159
+ any backfill. Records whose session no longer exists land in the **Unassigned** bucket.
160
+ Above 5000 records the aggregation runs in a Web Worker (built from the pure aggregator's own
161
+ source, so both paths are the same code); if the browser refuses the Worker it silently falls
162
+ back to the main thread, where 6000 records aggregate in ~6 ms.
163
+
85
164
  ## Pricing model
86
165
 
87
166
  Off-peak prices in **CNY per 1M tokens**; peak multiplies everything by **2**.
@@ -96,7 +175,7 @@ cost = (hit/1e6)*hit_price + (miss/1e6)*miss_price + (out/1e6)*out_price [×2
96
175
  ```
97
176
 
98
177
  **Peak** = Beijing time (UTC+8), Monday–Friday, 09:00–12:00 and 14:00–18:00, excluding statutory
99
- holidays. Everything else, plus weekends, is **valley**.
178
+ holidays. Everything else, plus weekends, is **off-peak**.
100
179
 
101
180
  Unrecognised model ids are priced as `deepseek-flash` (or `deepseek-v4-pro` when the id contains
102
181
  `pro`) and the UI marks the figure as an estimate. The table lives in `PRICES` at the top of
@@ -114,7 +193,12 @@ The host half registers three exact, read-only routes on the DSH web server:
114
193
  | --- | --- |
115
194
  | `GET /api/consumption/turns?sessionId=<id>` | per-turn `{turn, hit, miss, out, time, model}` (log-length cached) |
116
195
  | `GET /api/consumption/account` | current provider/model + whether the key is configured |
117
- | `GET /api/consumption/balance[?force=1]` | `{ok, available, currency, total, granted, toppedUp}`; `is_available:false` renders a red “insufficient balance” |
196
+ | `GET /api/consumption/balance[?force=1]` | `{ok, available, currency, total, granted, toppedUp}`; `is_available:false` renders a red "insufficient balance" |
197
+
198
+ Failures are `{ok:false, code, detail, error}`: `code` is language-neutral
199
+ (`no-credentials`, `no-key`, `key-read-failed`, `no-subprocess`, `curl-empty`, `spawn-failed`,
200
+ `bad-response`, `balance-request-failed`, `missing-session-id`, `log-unavailable`), `detail` is raw
201
+ text, `error` is an English fallback for logs.
118
202
 
119
203
  These routes are **unauthenticated**. If your DSH web server binds `0.0.0.0`, anyone on the
120
204
  network can read the balance and token figures; keep it on `127.0.0.1` unless you need otherwise.
@@ -138,10 +222,20 @@ Install the DSH plugin "dsh-consumption-badge" into my web profile and prove it
138
222
  `{"ok":true,...}`. Verify the client half:
139
223
  `curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:<port>/plugins/dsh-consumption-badge/client.js`
140
224
  must return `200`.
141
- 5. Open any session and confirm the `⛽ <tokens> | ≈¥<cost> | peak|valley` pill in the assistant
225
+ 5. Open any session and confirm the `⛽ <tokens> | ≈¥<cost> | Peak|Off-peak` pill in the assistant
142
226
  action row; click it and confirm the modal shows the four cards, the dual-axis chart, the
143
227
  balance, and that Esc closes it.
144
- 6. Report which step failed and paste the boot log lines that mention `consumption-badge`.
228
+ 6. Keep Settings → Usage open, then switch the shell language between Chinese and English
229
+ (Settings → General → Language) and confirm every visible string in the badge, the modal, the
230
+ settings page and the personalization block switches without a reload.
231
+ 7. On the Usage page switch the time range (Today / Last 7 days / Last 30 days / Last 12 months),
232
+ switch the workspace (All workspaces → a specific workspace → Unassigned), set the calendar range
233
+ and confirm both the chart and the footer totals update, and that the Token axis only appears
234
+ after clicking its legend chip.
235
+ 8. If `dsh-plugin-personalization` is installed, open Settings → Personalization and confirm a
236
+ Usage popup block appears at the bottom; switch it to Glass / Solid color / Custom image and
237
+ confirm the ⛽ modal (click a badge) re-renders with that background.
238
+ 9. Report which step failed and paste the boot log lines that mention `consumption-badge`.
145
239
 
146
240
  Rules while doing this: never write to the DSH credentials file yourself (ask me for
147
241
  DEEPSEEK_API_KEY), never bind a second server to the same port, and do not try to persist the
@@ -158,12 +252,16 @@ plugin by "running" it — a session-scoped dynamic plugin always dies with the
158
252
  `require("react")` — React must stay external (a second React instance breaks slot rendering).
159
253
  - `lib/index.js` imports nothing at all (no `@deepseek-ai/cordis`), so it does not depend on the
160
254
  profile being able to resolve Cordis; it exports plain `name` / `inject` / `apply`.
161
- - Self-check without booting DSH: `node check.mjs` (46 assertions over the route contract and the
162
- client bundle contract). It lives outside the published `files` list.
255
+ - Do not hard-code user-visible English or Chinese in new JSX: call `tt('…')` with the Chinese copy
256
+ as the key and add the English entry to `DICT_EN`. `check.mjs` fails if any `tt()` key has no
257
+ English translation or if a translation still contains Chinese.
258
+ - Self-check without booting DSH: `node check.mjs` (224 assertions over the route contract, the
259
+ client bundle contract, the pricing/aggregation maths and the bilingual copy). It lives outside
260
+ the published `files` list.
163
261
 
164
262
  ## Known limitations
165
263
 
166
- - All amounts are **estimates** — price table, peak/valley rules and model detection can drift
264
+ - All amounts are **estimates** — price table, peak/off-peak rules and model detection can drift
167
265
  from your actual invoice.
168
266
  - One chart point per turn; multiple steps in a turn are summed.
169
267
  - Balance parsing depends on DeepSeek's `/user/balance` response shape; failures only affect the
@@ -171,6 +269,7 @@ plugin by "running" it — a session-scoped dynamic plugin always dies with the
171
269
  - The chart covers turns present in the session log fold (whole log), while the pill's total uses
172
270
  the shipped `tokenUsage` projection — both are whole-session, but a heavily-compacted session may
173
271
  differ by the tokens lost to compaction.
272
+ - Only `zh` and `en` are shipped; any other shell locale falls back to `zh` (the dictionary keys).
174
273
 
175
274
  ## License
176
275