Skip to content

feat(client): plans & quota panel, session cost readout, price table remote (ported from #36) - #38

Merged
Mars-Sea merged 6 commits into
mainfrom
feat/pr36-panel-and-session-cost
Sep 14, 2026
Merged

Mars-Sea merged 6 commits into
mainfrom
feat/pr36-panel-and-session-cost

Conversation

@Mars-Sea

Copy link
Copy Markdown
Owner

Summary

Ports the three features of #36 onto the current main. #36's branch is based on a main that predates the #33/#34/#35 fixes and the #37 image budget, so its diff also reverts those and deletes the /commandcode command, Config.lang, the zh copy, and the bilingual error text — and it currently fails 21 of its own tests, including the published regressions. Rather than merge that, this PR re-applies only the new work on top of main.

Nothing in this diff reverts anything: src/adapter.ts, src/accounts.ts, src/client/sessions.ts, src/client/locales.ts and src/tui-settings.ts are untouched, and the settings page keeps its { zh, en } registration.

What lands

1. Vendored price table + commandcode/prices Remote (src/model-prices.ts, src/usage-wire.ts, src/usage-remote.ts)

  • The official per-token rates (input, output, cache-read, and cache-write where the page publishes one) plus the peak override for the hourly-priced models.
  • Joined to catalog ids by generated slug candidates, because the pricing page drops vendor prefixes and sometimes inserts a hyphen (Qwen/Qwen3.8-Max-0902qwen-3.8-max-0902); a row no catalog model claims is still served under its slug.
  • Served over the existing commandcodeUsage service, so the browser bundle carries no copy that could drift and a price update reaches an open page without a rebuild.
  • isPeakPricingHour() is split out of peakPricingState() in capabilities.ts so the browser prices against the same schedule the model picker labels, and the windows travel with the table.

2. Plans & quota panel (src/client/panel*.ts(x))

  • A sidebar footer card (order 1, directly above Settings) showing the serving account's plan and both quota windows with their own spend/limits; clicking it opens a dashboard in the layout's main slot (plan badge, billing period, both windows as bars with reset times, monthly credits derived the CLI's way, purchased/free balances).
  • Both surfaces render one projection and read the existing usage snapshot; startPanelAutoRefresh() is a refcounted 2-minute tick that no-ops while no credential is configured.

3. Composer session cost (src/client/session-cost*.ts)

  • Decorates the harness's own token-usage pill and usage dialog with dollar figures instead of taking the shipped stats cell over, so the token/cache-hit/throughput readout and its translations stay exactly as they are.
  • Prices only commandcode sessions; never invents a cache-write rate the page omits (those tokens are reported as unpriced and the total stays a floor); renders nothing — no pill at all — for no usage, no table, an unknown model, or all-zero buckets rather than a confident $0.00.

Compatibility

The panel and the readout need dsh 0.1.5+: the layout's main slot is named conversation before 0.1.5, and the injection's DOM anchors ([data-composer-stats], [data-session-stats-usage]) are 0.1.5 markup. On an older engine their slot declarations never exist, slots.inject never fires, and every other surface (settings page, Models-page card, /commandcode, TUI section) is unaffected.

Both new surfaces are English-only by construction rather than through ctx.locale — kept as authored and documented in AGENTS.md, since it is a product decision rather than an implementation detail.

Changes made while porting

Tests

npm test447 passing (396 before, 51 added), npm run typecheck clean, npm run build clean.

The new tests pin behaviour rather than coverage:

  • tests/model-prices.test.ts fails whenever a catalog model has no price row — the visible decision point when upstream adds a model — and pins the slug join, the free rows, and that a missing cache-write rate stays missing.
  • tests/session-cost.test.ts pins the three honesty rules (peak/off-peak selection, never guessing a cache-write rate, invisible-when-unpriceable) and the positional dialog decorations.
  • tests/panel.test.ts pins the projection (dedup, serving-account selection, over-cap percentages, uncapped windows, the CLI's monthly derivation, the failure boxes) and the shared refresh loop.
  • tests/prices-client.test.ts pins the one-shot cache and the no-endpoint Host.
  • tests/client-boot.test.ts now asserts the real apply() seats all four surfaces and that the stats cell keeps its id.

Credit

Authored by @xer-on, whose #36 this is ported from — the commit preserves them as the git author. The design comments in the new modules are theirs and were kept verbatim.

…remote

Ports the three features of #36 onto the current main instead of merging its
branch, whose base predates the #33/#34/#35 fixes and the #37 image budget,
and whose diff also deleted the `/commandcode` command, `Config.lang`, the
zh copy and the bilingual error text. Nothing here reverts any of that: the
adapter, accounts, sessions, locales and tui-settings modules are untouched,
and the settings page keeps its zh/en registration.

What lands:

- `src/model-prices.ts` vendors the official per-token rates (input, output,
  cache-read, cache-write where published) plus the peak override for the
  hourly-priced models, joined to catalog ids by generated slug candidates.
- The price table crosses to the browser over a new `commandcode/prices`
  Remote on the existing `commandcodeUsage` service, so the bundle carries no
  copy that could drift. The endpoint is optional on both sides: a Host older
  than this feature lands in a permanent "no prices" state, never a throw.
- `src/client/panel*.ts(x)` renders one projection as a sidebar footer card
  (5-hour + weekly spend against their limits) and a center dashboard, on a
  refcounted 2-minute refresh tick.
- `src/client/session-cost*.ts` decorates the harness's own token-usage pill
  and usage dialog with dollar figures rather than taking the `stats` cell
  over, prices only commandcode sessions, never invents a cache-write rate,
  and renders nothing at all when a session cannot be priced.
- `isPeakPricingHour()` is split out of `peakPricingState()` so the browser
  prices against the same schedule the picker labels, and the windows ship
  with the table.

The panel and the cost readout need dsh 0.1.5+ (`main` and the injection's DOM
anchors are 0.1.5 additions); on an older engine their registrations silently
do not happen and every other surface is unaffected. Both surfaces are
English-only by construction, as documented in AGENTS.md.

Ported-from: #36
Original-author: xer-on <rifat.rahman378@gmail.com>
Review findings on the three ported surfaces, plus one bug the new
DOM tests found. Behaviour fixes first:

- The hourly-priced `deepseek-v4-flash-vision-exp` row overstated every
  user-visible cost by ~47%: it carried 0.22/0.66/0.007 (peak 0.44/1.32)
  where the official page publishes 0.15/0.60/0.003 (peak 0.30/1.20) —
  identical to plain `deepseek-v4-flash`. The stale prose in
  capabilities.ts that seeded the wrong figures is corrected too.

- `scripts/sync-model-prices.mjs` is the generator `model-prices.ts`
  claimed exists. It re-reads the page's embedded JSON, asserts the
  off-peak duplication and the peak ordering, prints a warning for every
  model carrying `contextTiers` (the row stores the base band, so those
  figures are a floor), CROSS-CHECKS each rewritten row against the
  page's own rendered table, and rewrites only the rows. `--check`
  reports drift without writing and exits 2 when the page cannot be
  read, so a network failure never reads as "up to date". The
  cross-check is what catches a row that is internally consistent and
  still wrong — the class of error above.

- `report.credits` no longer conflates "not reported" with "zero". An
  absent monthly balance used to arrive as `0` and become a confident
  "100% used, quota exhausted" with purchased/free tiles showing
  balances that were never fetched. `monthlyReported` is
  optional-tri-state and `fiveHour`/`weekly` are optional members, so
  the panel and `/commandcode` draw a window row only for a window the
  endpoint really reported, dash an unreported figure, and keep a
  reported `cap: 0` as uncapped spend. The settings card and the
  command skip absent windows the same way.

- The panel's plan-name fallback no longer contradicts its own header,
  and an uncapped window is labelled "unlimited" instead of printing a
  bare amount beside a fabricated 0%.

- Session cost: a session whose every billed token is unpriced renders
  nothing instead of a confident `$0.00` (a real cache-write-only case);
  the guard keys on "no rate", never on "the total rounds to zero", so
  a genuine sub-cent session keeps its `<$0.0001` bound. A cache-write
  row whose rate IS published is no longer hidden from the dialog — its
  cost was already in the total. Empty tooltip clauses are dropped
  rather than joined into `·  ·`.

- `pruneDialog()` now restores a hidden row's inline style before
  dropping a detached cell. React reuses DOM nodes across a remount, so
  a surviving `display: none` could hide a row of a dialog this module
  no longer decorates, with nothing left tracking it. Found by the new
  display tests.

- The sidebar card's version floor was documented wrong in four places
  and is now enforced in code. Measured across 0.1.1-rc.2 … 0.1.5-rc.2,
  `sidebar.footer.action` and `conversation.composer.dock` exist and
  render in every one of those releases; only the keyed `main` seat
  (0.1.5-alpha.2) and `layout.selectPanel` (0.1.5-rc.1) are new. An
  ungated card would therefore render on those engines and do nothing
  when clicked, so the footer registration is gated on the layout seam.
  `package.json` stays untouched: its compatibility list already names
  exactly the releases where the panel works.

- `PANEL_CSS_ID` still carried the fork's package name in the shipped
  bundle; it is `@mars-sea/…` now, matching the stylesheet id the
  settings page uses.

Tests: 468 passing (up from 447). `tests/session-cost-display.ts` adds
coverage for the 491-line DOM injection layer, which had none — it
drives the real class through its `doc`/`observe` seams against a fake
DOM and pins the confirmation rule, self-healing, hide/restore and
disposal. `tests/client-boot.test.ts` now models the slot DECLARATION
set and the layout seam separately, which is what the old always-fire
stub hid. The panel's monthly-derivation test was circular (limit ==
remaining, so `$0.00` passed for a hardcoded zero) and now uses the
live API's 30/8.68 fixture.

Docs: README + README.zh-CN list both new surfaces and their 0.1.5
requirement; AGENTS.md corrects the version boundary, names
`panel-styles.ts`, records the credits tri-state and the sync script.
Mars-Sea and others added 3 commits September 14, 2026 20:44
Follow-up to the review of the previous two commits: no behaviour changes, so
the 486-test suite and the generated bundle are unchanged in substance.

- Record the ONE real gap in the durable cost facts: the fold implements the
  0.1.5 token-meter replacement rule (`llm/retry-started` closes the slot, so a
  retried attempt adds), while the 0.1.2/0.1.3-era fold replaced on
  `(turn, step)` alone and never handled that event. On those engines the two
  folds disagree by the retried attempt's tokens, and the client's per-bucket
  equality gate against `tokenUsage` then hides the readout instead of showing
  a wrong figure. Retries are routine on this route and both versions are
  declared compatible, so it is a live gap rather than a theoretical one. The
  note also says why the equality gate must NOT be loosened to work around it.

- Record why the settings usage card reads availability from the Host report:
  a composition literal is a stripped secret and the CLI auth file is not in
  the credentials store, so both are invisible to the browser while the Host
  serves requests with them. `state.anyAccountConfigured` must not gate that
  card or the post-save refresh, and `shouldRefresh` stays `idle`-only so a
  failed fetch cannot become an automatic request loop.

- `requestRates()`: document that a tiered row returns its band and does not
  then apply `peak` (the page publishes the two dimensions independently and no
  row carries both today), and that the band `find` is order-dependent on
  purpose because both the generator and the wire parser reject non-ascending
  bounds or a bounded last band.

- Fix two stale claims this work left behind: the controller JSDoc was attached
  to `PriceRetryTimer` and still called the cache "one-shot" (it now has a
  bounded 1/2/4 s retry budget and a rebind reload), and `ensure()` claimed the
  composer mounting was a trigger when its only callers are the namespace
  landing and a manual refresh. Same for the two AGENTS.md one-liners, plus
  the module/test entries the layout block was missing.

- Note that `isPeakHour()` in `src/client/session-cost.ts` is a dead third copy
  of the weekday rule (only its own test calls it; it is DCE'd out of the
  bundle) so the next reader deletes it rather than re-wiring it.
…rfaces

Fixes the review findings on #38, including the three follow-up defects (zero subtotal for partly-free sessions, settings-card credential gating, /commandcode balance ratio) and the documentation of the v1/v2 fold divergence.
@Mars-Sea
Mars-Sea merged commit c27bc03 into main Sep 14, 2026
4 checks passed
@Mars-Sea
Mars-Sea deleted the feat/pr36-panel-and-session-cost branch September 14, 2026 15:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants