Bundle
@local/token-breakdown-stats
Session token usage with subagent rollup: replaces the composer-dock stats pill so the bottom total covers the main agent plus every subagent it called, and the click-open dialog itemizes usage by bucket, by subagent, and by model.
- Source
- Minggle
- License
- MIT
- Updated
- Updated 8 days ago
Readme
# dsh-token-breakdown-stats
[](https://github.com/topics/dsh-plugin)
[](https://github.com/deepseek-ai/deepseek-harness)
[](LICENSE)
English | [中文](README.zh.md)
A [dsh](https://github.com/deepseek-ai/deepseek-harness) plugin (`dsh-plugin`
topic) that replaces the composer-dock statistics pill so a session's token
total covers the **main agent plus every subagent it called**, with a
click-open breakdown by billing bucket, by subagent, and by model.
## Summary
The composer-dock statistics pill in a dsh session reports only the **main
agent's** token usage. When that agent delegates to subagents, every token the
subagents spend is invisible in the session footer.
This plugin replaces that pill so the footer reports the **session total —
main agent plus every subagent it called** — and its click-open dialog
itemizes the usage by billing bucket, by subagent, and by model.
## What changes
| | shipped pill | this plugin |
|---|---|---|
| Pill label | main-agent total + cache hit | **session total** (main + subagents) + cache hit + subagent count |
| Dialog | four billing buckets | buckets, main/subagent split, subagent aggregate, per-model rows |
| Time/activity pill | turns, steps, speed, timings | unchanged (reproduced) |
Subagents are reported as an **aggregate** everywhere: the pill shows how many
there were, and the dialog folds them into one block (count, how many are still
running, share of the session, and the summed buckets). No per-subagent rows —
a session can spawn dozens, and the per-child detail already lives in the
subagent catalog in the session header.
The dialog shows:
- **总用量** — the four disjoint billing buckets (uncached input, cache read,
cache write, output) plus cache hit, over the whole session tree.
- **主 Agent** — the provider/model routes observed in the loaded turns, and
that session's own buckets.
- **子 Agent(N)** — one aggregate block: how many were discovered, how many
are still running, their share of the session total, and the folded buckets.
- **按模型** — per provider/model totals folded from the loaded turns.
- A friendly note when the session has not called any subagent yet.
## How it works
The plugin registers the same dock cell the shipped `ui-chat` row uses —
`conversation.composer.dock`, id `stats`, order `0` — so it **replaces** the
built-in pill rather than adding a second one. A list slot rejects a second
registration that reuses an id at the same priority, so this row passes
`priority: -1`, strictly below the shipped row's 0 (lowest renders).
Every figure rides durable projections, so paging and compaction cannot change
them:
- `tokenUsage` — the four disjoint provider billing buckets per session.
- `subagentCatalog` — direct children of a session, in parent event order,
with mode and optional label.
- `SessionSummary.projectionValues` — the same projections plus the subagent
identity (`label`) and timing, addressed by session id.
- `ChatSnapshot.nodes.values()` — the keyed store of every materialized Chat
Node. `turn-tail` rows deliberately make no contribution to `legacy.nodes`,
so this is the only place the per-turn `routes` (provider/model) can be read
from; the legacy slice still feeds the step-count fold.
The `compact` / `detailed` preference is read from the same `ui-chat` config
scope the shipped pill uses; `compact` keeps the plain speed and cache-hit
readings without dialogs.
## Known limitations
- **Per-subagent model attribution is unavailable.** The client-side
`tokenUsage` projection carries only the four buckets — no route — so a
subagent's row shows its total, not its model. Model attribution covers the
main session's loaded turns.
- **Descendants are a best-effort walk.** `subagentCatalog` is the direct
children of a session; grandchildren appear only once their own catalog has
been loaded (the same rule the shipped subagent catalog follows). Nothing is
invented for unloaded levels.
- **Model attribution covers loaded turns.** `turn-tail` rows exist only for
closed turns inside the loaded window, so on a long, paged session the model
row and the by-model section can lag the durable totals. In this version the
chat target's assistant nodes carry no `providerMetadata`, so there is no
second source to fall back on — the row shows the session's models only once
at least one closed turn is loaded.
- **Compact mode** keeps the shipped behavior (speed and cache hit only).
## Install
The bundle is a plain directory — no build step is required, because
`client.js` is committed. Install it into a profile from a local clone:
```powershell
git clone https://github.com/Minggle/dsh-token-breakdown-stats.git
```
Then, in a dsh session, install the bundle with `plugin_manager`:
```
plugin_manager action: install_bundle
target: <absolute path to the cloned directory>
```
The inserted row id is `token-breakdown-stats`. Restart the app (or let the
live patch reload apply it) and the pill in the composer dock is replaced.
To rebuild `client.js` from `src/` after editing the sources:
```powershell
node build.mjs
```
## Verify
```powershell
node src/smoke.test.cjs
```
The smoke test loads the built `client.js` through a mocked
`window.__ModuleLoader__` and a minimal React runtime, then asserts the
rollup math, the pill label, the dialog sections, Escape dismissal, compact
mode, and locale parity.
## Files
- `index.js` — Host half (no Host row is needed).
- `client.js` — generated browser artifact (do not edit).
- `src/entry.js` — slot registration and mode hook.
- `src/stats-pills.js` — the two pills and the breakdown dialog.
- `src/token-aggregator.cjs` — pure aggregation and route folding.
- `src/smoke.test.cjs` — Node smoke test.
- `locale/{en,zh}.json` — dictionaries (single source of truth).
- `icon.svg` — original artwork.
- `build.mjs` — inlines `src/` into `client.js`.
## Requirements
- DeepSeek Harness `>= 0.2.0-rc.2` (the plugin reads `ChatSnapshot.nodes`,
`SessionSummary.projectionValues`, and the `conversation.composer.dock`
slot's list-cell priority rule).
- A desktop or web profile; the Web GUI renders the pill.
## Install incident (2026-10-15)
An early build of this plugin made the desktop app fail web boot with
`1 entry did not activate: @deepseek-ai/dsh-client-ui-chat: failed`. The
bundle was removed and the profile restored from the backup the app itself
wrote (`cordis.patch.yml.bak-*`), so no user data was lost.
The root causes are recorded here because both are easy to reintroduce:
1. **A list slot rejects a same-id, same-priority registration.** The shipped
`ui-chat` row occupies `conversation.composer.dock` id `stats` at priority
0; a replacement must pass a strictly lower `priority` (this plugin uses
`-1`). The slot's `register` throws
`already has an entry with id "stats" at priority 0 … register at a
different priority to shadow it (lowest renders)`, and because the plugin's
inject callback runs inside the dock owner's fiber, the failure is reported
against that owner rather than the plugin.
2. **A restricted Client context throws on an undeclared service.** Reading
`ctx.configForms` without declaring `configForms` in the entry's `inject`
raises `cannot get property "configForms" without inject`. The plugin now
declares only `['slots', 'locale']` and reads everything else defensively.
Two smoke-test mocks reproduce both failure modes so a regression fails the
test instead of a boot: the context throws on undeclared service access, and
the slot service rejects a duplicate cell at the same priority.
## License
[MIT](LICENSE)
Install
dsh plugin --profile web add github:Minggle/dsh-token-breakdown-stats
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install local-token-breakdown-stats from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.