# dsh-cost-gauge [ไธญๆ–‡](README.md) | English A **cost gauge** for DeepSeek Harness (`dsh`): a square floating window at the **upper left** of the Web UI that shows the current session cost and account balance in real time. The **semicircular dial** has a hand that follows Beijing time and arcs colored by the day's rate bands (green = standard, amber = peak, all green on weekends). A red lamp at the top blinks when the balance drops below the threshold. Supports **drag / resize** and **minimize** (status lamp + countdown ring + model badge). > ๐Ÿ”€ **Related project**: the โ€œminimal-clock / multi-skinโ€ line is maintained separately as **[dsh-cost-gauge-plus](https://github.com/wjingshan/dsh-cost-gauge-plus)**. This repository keeps evolving the **v1.0 classic square gauge**; the two are independent. ## Screenshots | Expanded | Minimized | | --- | --- | | Expanded: time dial with cost/balance | Minimized: status lamp, countdown ring, cost/balance and model badge | ## What's new in v1.6.0 | Before / after (left: off ยท right: on โ€” only the part over the chat text changes) | | --- | | Before/after: the widget straddles the chat column and only the part inside it turns into grey glass | - **A new glass treatment**: the part of the widget reaching into the conversation **content column** turns into **grey glass**: - **title bar / bottom cost & balance rows**: slightly transparent (adjustable); - **the middle background**: fully transparent โ€” the blurred conversation shows through; - **the dial and text in the middle**: keep only their **lightness** (hue and saturation dropped = greyscale), and **each colour's opacity equals its lightness** (lightness 80 โ†’ opacity 80%), so different colours end up differently transparent. - **Only the overlapping rectangle is affected**: whatever has not entered the chat column stays exactly as it was (even the dial keeps its colours). Implemented by clipping inside the SVG filter โ€” the transformed result inside the hole and the original outside, feathered by the **Edge fade** setting. - **Six settings** (all live and remembered): | Setting | Range | Default | Meaning | | --- | --- | --- | --- | | Frost | 0โ€“24px | 9px | blur radius | | Transparency | 0โ€“100% | 47% | density of the milky veil in the middle | | Edge fade | 0โ€“40px | 14px | transition width between the normal and the glass area (0 = hard edge) | | Bar fade | 0โ€“60% | 15% | transparency of the title bar and the bottom rows (**0% = fully opaque**) | | Content opacity | 0โ€“100% | 100% | multiplier on top of lightness: 100% = strictly by lightness, 0% = invisible (linear, every step does something) | | Lightness | Rec.709 / HSL / HSV | Rec.709 | which lightness defines the greyscale and the opacity | - **Fixed**: changing *Content opacity* or switching *Lightness* rebuilt the filter and reset the hole rectangle, so the transform vanished until the next tick (it felt like the slider did nothing). It now applies immediately โ€” and the range was narrowed from 0โ€“200% to **0โ€“100%**. ## What's new in v1.5.3 - **Fixed: session spend could stop updating (out-of-range ledger cursor)**: after a session log is compacted or rewritten its seq space shrinks, while the persisted `cursor` stays in the old space โ€” so `snapshotEvents(cursor, log end)` returns an empty array forever and new events are never folded in (session spend freezes, or stays at ยฅ0.00 after a reset). The session's ledger entry is now rebuilt from the current log whenever `cursor > session.seq`. - **Fixed: the dial hand ran backwards after midnight**: the 00:00โ€“06:00 branch of `dialPosOfHour()` had the wrong direction (`90 - 15t`), so the hand turned back after reaching the top. Both halves now sweep left โ†’ top โ†’ right: night **18:00 left โ†’ 24:00 top โ†’ 06:00 right**, day 06:00 left โ†’ 12:00 top โ†’ 18:00 right, with instant jumps at 06:00 and 18:00. - **Version shown at the bottom of the settings panel**: the host returns `version` (read from `package.json`) in `/state`, and the settings panel shows `dsh-cost-gauge vX.Y.Z` in 10px muted text; the row hides itself when no version is available. ## What's new in v1.5.2 | Settings: frost strength / transparency | Before and after (left: off, right: on) | | --- | --- | | Settings: frost strength and transparency sliders | Frosted glass before/after: the title bar stays untouched, the area below fades into translucent frost | - **Adjustable frost strength and transparency** (two sliders in the settings panel, applied live and remembered): - **Frost** `0โ€“24px` โ€” blur radius, higher is hazier (0 keeps only the translucent tint); - **Transparency** `0โ€“100%` โ€” higher is more see-through and less milky. - **The title bar is never frosted**: the frost only applies below it, so the drag handle stays crisp for locating and grabbing the widget. - **Soft gradient edges instead of a hard line**: the transition width follows the frost strength (5โ€“18px). The hole is built as "horizontal complement โˆช vertical complement" (De Morgan) in the background mask, and the frost layer uses the same shape as its mask, so both fade out together. ## What's new in v1.5.1 | Same spot, side by side: off (left) / frosted (right) | | --- | | Frosted glass before/after: plain widget on the left, translucent frosted overlap on the right | - **The frost is actually visible now**: the layer used to be just 5% white plus `blur(12px)` โ€” blurring a flat dark chat background yields the same flat dark background, so nothing seemed to change. It is now a **milky gradient + slight brightening + inner hairline** (`blur(9px) saturate(1.2) brightness(1.12)`): measured on the same patch the luminance goes **29 โ†’ 83**, while the blurred text behind keeps its structure. - **Can't find the switch?** While the frost is off, covering the conversation text (overlap โ‰ฅ 35% of the widget) now shows a **one-time hint**: enable โ€œFrosted glass over the chat areaโ€ in settings. - **Fixed: the widget could get stuck in the top-left corner** โ€” loading in a minimized window / background tab (viewport 0ร—0) made the viewport clamp compute a position of (0,0) and persist it; the clamp is now skipped when the viewport is 0. ## What's new in v1.5.0 | Settings: the new โ€œfrosted glass over the chatโ€ switch | | --- | | Settings: balance threshold, default Excel folder and the new frosted-glass switch | - **Auto step-aside when the window is resized**: after you stretch or resize the window, if the widget covers the middle conversation **text column** it moves into the **blank gap between the text column and the sidebar** โ€” centred in that gap and placed in its **lower part** (flush with the bottom of the window; if that would cover the composer it stops 8px above it). Only when the gap is too narrow does it fall back to the sidebar column. - Priority: **middle text column (never covered) > bottom composer > sidebars**; ties keep "left stays left, right stays right" with the smallest movement. - Triggered only by **window resizes** (150 ms debounce); it never fights a manual drag and is skipped in the narrow strip mode. - The gap fits when one side's clearance is โ‰ฅ widget width + 16px: at the default 216px that needs a window โ‰ณ1658px (middle column โ‰ฅ 1378px, sidebar expanded, right bar closed); at the minimum 180px it is about 1458px. - **Optional frosted glass over the chat** (new setting, **off by default**): when enabled, the part of the widget overlapping the conversation text turns into translucent frosted glass (`backdrop-filter`) so the covered text stays readable โ€” only the genuinely overlapping part is frosted, parking in the blank gap keeps it opaque. Implemented as "punch a hole in the background layer and lay a frost layer over it" (`mask-composite: exclude`). - **Auto-dock when the sidebar collapses**: when DSH collapses the sidebar (or the viewport is very narrow) the narrow strip docks to the **right of the sidebar**; with the sidebar expanded it docks under the "Sessions / Workspaces" heading, and it returns to its previous spot when narrow mode ends (the dock position is never persisted). - Narrow strip width 48px โ†’ **36px**. - Internal fix: the "chat content column" used by both behaviours is now derived from `--dsh-chat-content-width` (the centred content band inside the panel) with proper `clamp()/calc()` resolution, falling back to the composer card width and then to the whole middle panel, so the blank gutters are no longer mistaken for conversation content. ## What's new in v1.4.0 | Narrow window โ†’ vertical mini | | --- | | Vertical mini: status lamp + cost + balance + model badge | - **Viewport-aware vertical mini**: a `ResizeObserver` watches the viewport width; at **779px or less** the gauge switches to a 48px-wide **vertical mini** (top to bottom: status lamp โ†’ session cost โ†’ balance โ†’ model badge, rendered vertically) and **expands again on click**. When the window grows back to **860px or more** the previous expanded/collapsed state is restored. - **Hysteresis** (enter 780 / exit 860) prevents flicker at the threshold; after you manually expand from the mini strip it stays expanded until the window grows past 860px again. - Thresholds can be overridden via `localStorage` keys `dsh-cost-gauge:narrowEnter` / `dsh-cost-gauge:narrowExit`. ## What's new in v1.3.0 | Spend records panel | Settings | Arc cap fix | | --- | --- | --- | | Records panel: ranges, chart, detail table | Settings: balance threshold + default Excel folder | Peak arc round cap before/after | - **Correct cost math**: the host now replays the session log and prices every usage event at โ€œrate at that moment ร— model at that momentโ€, so off-peak usage is charged at the off-peak rate and peak usage at the peak rate, then summed. This fixes the old behaviour where entering the peak window re-priced the whole history (cost doubling). - **New โ€œRecords / Resetโ€ icon buttons** (top-left and top-right of the dial; borderless mini icons with hover hints). The records panel offers `All time / Year / Month / Week` filtering and paging, a **stacked bar chart** (by band or by model) and a detail table that lists **only periods with usage**. Scope switches between `This session` and `All sessions`. The panel is draggable, may cover the gauge, and is pulled back inside the window when out of bounds. Reset is a two-step confirmation that moves the baseline and restarts from ยฅ0.00 (history is kept). - **Excel export is now a real `.xlsx`** (OOXML; hand-written minimal zip writer, no dependencies; opens in Excel/WPS with no format warning): two worksheets plus a note row, and **default file name `_-.xlsx`**. A default save folder can be configured in Settings (exports then write directly with no dialog); `๐Ÿ“‚ Open` reveals the file in Explorer. - **Host-side accounting**: the session event log is replayed every 15 s and persisted to `~/.dsh/cost-gauge/ledger.json`, so accounting continues while the dashboard is closed and records cover the session's full history. - **Dial arc cap fix**: peak (amber) segments are inset by the round-cap radius (5.43ยฐ) at interior boundaries (9:00 / 12:00 / 14:00) so the cap's outer edge lands exactly on the boundary instead of covering the neighbouring green segment; the 18:00 end keeps its original rounded look. - **Bilingual UI**: follows the DSH client language setting, falling back to the system/browser language (`zh*` โ†’ Chinese, otherwise English). ## Features - ๐Ÿ”ฒ **Floating window** โ€” sits near the upper left by default, drag it by the title bar; the position is remembered. - ๐Ÿ“ฑ **Viewport-aware** โ€” when the viewport gets narrow (โ‰ค 779px) the gauge collapses into a **36px vertical mini** (lamp + cost + balance + model badge); click to expand, and it restores automatically at 860px or more. If DSH auto-collapses the sidebar the strip docks to its right, and with the sidebar expanded it docks under the โ€œSessions / Workspacesโ€ heading. - ๐ŸชŸ **Auto step-aside** โ€” on window resizes, a widget covering the conversation text moves into the blank gap between the text column and the sidebar (centred, lower part); priority is text column > composer > sidebars. - ๐ŸงŠ **Glass over the chat** (six settings) โ€” the part reaching into the chat text turns into grey glass: translucent bars, a fully transparent middle background and a dial whose opacity equals its lightness; **the rest of the widget is untouched**. - ๐Ÿ’ฐ **Session cost** โ€” token usage priced with the official peak/off-peak rates (cache miss / cache hit / output priced separately). - **Per-event pricing**: usage produced during off-peak hours is charged at the off-peak rate and usage produced during peak hours at the peak rate, then summed. Entering the peak window does **not** re-price earlier off-peak usage. - ๐Ÿงญ **Rate hand** โ€” the dial shows the current rate band and the countdown to the next switch. - Peak (doubled): Monโ€“Fri 09:00โ€“12:00 and 14:00โ€“18:00 Beijing time - Off-peak (standard): all remaining hours, including the whole weekend; half the peak price - ๐Ÿ”ด **Low-balance alarm** โ€” the top lamp blinks red when the balance is below the threshold (default ยฅ10), green otherwise. - โš™๏ธ **Configurable threshold** โ€” click the gear to change the alert threshold; it takes effect immediately and is remembered (localStorage). - ๐Ÿ—’ **Spend records** (top-left icon on the dial) โ€” a wide panel with **All time / Year / Month / Week** range filtering and paging, a **stacked bar chart** (by peak/off-peak or by model), a detail table and totals. Scope can be switched between **This session** and **All sessions**; the detail table lists only periods that **have usage records** (the chart keeps the full timeline). The panel can be dragged, may cover the gauge, and is automatically pulled back inside the window when it would go out of bounds. - โฌ‡ **Export to Excel** โ€” exports the current range as a **real `.xlsx`** (OOXML; a hand-written minimal zip writer, no third-party dependencies; opens directly in Excel/WPS with no format warning). The workbook contains **two worksheets**: `By peak/off-peak` and `By model`; the first row of both sheets is an export note (session name, billing period, range filter, last reset, export time). **Default file name = `_-.xlsx`** (e.g. `่Šฑ่ดนๆŒ‡็คบๅ™จๆ’ไปถ_20260808-20260911.xlsx`; for all sessions `All sessions_...`). After a successful export, `๐Ÿ“‚ Open` reveals the file in Explorer. - ๐Ÿ“ **Default save folder** โ€” once a folder is configured in Settings, exports are written **directly with no dialog**; when unset, a native folder picker appears and the chosen folder becomes the default (preferences persist in `~/.dsh/cost-gauge/ledger.json`). - โ†บ **Reset session cost** (top-right icon on the dial) โ€” after a two-step confirmation the current total becomes the baseline and the counter restarts from ยฅ0.00 (history is not deleted). - ๐Ÿงฎ **Host-side accounting (log replay)** โ€” every 15 s the host replays each session's **event log** (`assistant/message` / `assistant/attempt` carry usage and a timestamp; `request/header` provides the model in force) and books each entry at โ€œrate at that moment ร— model at that momentโ€ into โ€œday ร— band ร— modelโ€, persisted to `~/.dsh/cost-gauge/ledger.json`. `llm/retry-started` and repeated samples for the same turn/step **replace** rather than accumulate, matching the official projection. Accounting therefore continues while the dashboard page is closed, and records cover the session log's **full history** (not just since installation). - ๐ŸŒ **Bilingual UI** โ€” the plugin follows the DSH client language (Settings โ†’ General), and falls back to the system/browser language: Chinese when it starts with `zh`, otherwise English. ## Install ### One-liner (recommended, no git needed) Paste into PowerShell and press Enter (it adds `dsh` automatically): ```powershell irm https://raw.githubusercontent.com/wjingshan/dsh-cost-gauge/main/install.ps1 | iex ``` > The one-liner installs the **latest stable Release tag** automatically. To install the development version or pin a tag, download the script first: ```powershell irm https://raw.githubusercontent.com/wjingshan/dsh-cost-gauge/main/install.ps1 -OutFile install-dsh-cost-gauge.ps1 .\install-dsh-cost-gauge.ps1 -Ref main # development (main) .\install-dsh-cost-gauge.ps1 -Ref v1.5.0 # pin a tag ``` ### Manual install ```sh # recommended: follow the main branch โ€” the plugin market can offer and apply updates dsh plugin --profile web add github:wjingshan/dsh-cost-gauge#main # local directory (linked; edits to lib/*.js take effect after a page refresh) dsh plugin --profile web add link:/path/to/dsh-cost-gauge # pinned version (not recommended โ€” see the note below: the market will not update it) dsh plugin --profile web add github:wjingshan/dsh-cost-gauge#v1.6.0 # tarball, no git required (the market never offers updates for this form) dsh plugin --profile web add https://github.com/wjingshan/dsh-cost-gauge/archive/refs/tags/v1.6.0.tar.gz ``` > โš ๏ธ **About the market's "Update"**: for git installs it decides by **whether the commit changed**, not by version. > - โœ… **To keep updates working**: install a **branch** form (`#main`, or `github:owner/repo` with no ref). > - โŒ **Do not install a fixed tag** (`github:owner/repo#v1.6.0`): the update command re-adds the same tag, so it always fails with "the update command completed but the version did not change" โ€” retrying never helps; you have to change the spec by hand. > - A **Release tarball URL** install is never offered an update by the market (no comparable commit in the lockfile); reinstall manually to move forward. After installing, **restart** `dsh web` and refresh the page: ```sh dsh web ``` ## Configuration The balance threshold can be changed from the gear in the floating window or overridden in the profile's `cordis.patch.yml`: ```yaml - update: - id: cost-gauge config: threshold: 10 # balance alert threshold (CNY) baseUrl: 'https://api.deepseek.com' apiKeyEnv: 'DEEPSEEK_API_KEY' refreshSeconds: 30 # balance query cache, seconds ``` > A patch replaces a row's whole `config` object, so restate every key you need. ## API | Endpoint | Purpose | | --- | --- | | `GET /api/cost-gauge/state?session=` | balance + session cost (since last reset) + rate + threshold | | `GET /api/cost-gauge/balance` / `GET /api/cost-gauge/refresh` | balance view / force refresh | | `GET /api/cost-gauge/records?session=&scope=session\|all` | daily records for one session or merged across sessions | | `POST /api/cost-gauge/reset` | reset the session cost baseline | | `GET/POST /api/cost-gauge/prefs` | read / write export preferences (default folder) | | `POST /api/cost-gauge/pick-folder` | native folder picker, saved as the default | | `POST /api/cost-gauge/export` | write the `.xlsx` (two worksheets) into the configured folder | | `POST /api/cost-gauge/open-folder` | reveal the most recent export in Explorer | ## Data & security - The balance is read through the official `GET /user/balance`; the API key is resolved on the host only (credentials seam / environment variable) and is **never sent to the browser**. - Cost comes from replaying the session event log on the host with the official peak/off-peak rates; cache writes are not billed separately (matching the official rule). - Pure ESM, no third-party runtime dependencies: the host uses Node built-ins only, the browser half is plain JavaScript (no React). ## Directory layout ``` dsh-cost-gauge/ โ”œโ”€โ”€ package.json # dsh.bundle (host) + dsh.client (browser) declarations โ”œโ”€โ”€ cordis.patch.yml # plugin row (with default config) โ”œโ”€โ”€ install.ps1 # one-liner installer โ”œโ”€โ”€ release.ps1 # commit + bump + push + GitHub Release โ”œโ”€โ”€ docs/ # screenshots, preview pages, Alipay QR โ”œโ”€โ”€ lib/ โ”‚ โ”œโ”€โ”€ index.js # host: balance, accounting, rate, /api/cost-gauge/* routes โ”‚ โ”œโ”€โ”€ ledger.js # host: log replay accounting, ledger persistence, xlsx writer โ”‚ โ””โ”€โ”€ client.js # browser: floating gauge + records panel (bilingual) โ””โ”€โ”€ README.md / README.en.md ``` ## Release ```powershell .\release.ps1 -Type minor -Message "feat: ..." # or -Version 1.3.0 ``` Creating the GitHub Release needs a PAT (`GH_TOKEN`, fine-grained with Contents read/write) or an interactive prompt. `install.ps1` always picks the latest Release tag, so no script changes are needed per release. ## License MIT --- ## โ˜• Sponsor If this plugin helps you, you are welcome to buy me a coffee โ˜• Alipay QR code
**Thanks for your support!** ๐Ÿ’™