feat(client): plans & quota panel, session cost readout, price table remote (ported from #36) - #38
Merged
Merged
Conversation
…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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ports the three features of #36 onto the current
main. #36's branch is based on amainthat predates the #33/#34/#35 fixes and the #37 image budget, so its diff also reverts those and deletes the/commandcodecommand,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 ofmain.Nothing in this diff reverts anything:
src/adapter.ts,src/accounts.ts,src/client/sessions.ts,src/client/locales.tsandsrc/tui-settings.tsare untouched, and the settings page keeps its{ zh, en }registration.What lands
1. Vendored price table +
commandcode/pricesRemote (src/model-prices.ts,src/usage-wire.ts,src/usage-remote.ts)Qwen/Qwen3.8-Max-0902→qwen-3.8-max-0902); a row no catalog model claims is still served under its slug.commandcodeUsageservice, 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 ofpeakPricingState()incapabilities.tsso 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))mainslot (plan badge, billing period, both windows as bars with reset times, monthly credits derived the CLI's way, purchased/free balances).startPanelAutoRefresh()is a refcounted 2-minute tick that no-ops while no credential is configured.3. Composer session cost (
src/client/session-cost*.ts)statscell over, so the token/cache-hit/throughput readout and its translations stay exactly as they are.commandcodesessions; 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
mainslot is namedconversationbefore 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.injectnever 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 inAGENTS.md, since it is a product decision rather than an implementation detail.Changes made while porting
REMOTE_PACKAGE, the update check, and the injected CSS ids at@xer-on/dsh-commandcode-provider. The package is@mars-sea/..., so a Remote mounted under the fork name never resolves — the price table would silently never arrive. All of those are@mars-seahere.injectPanelCsswas called but never defined in feat(client): add plans & quota sidebar panel, real-time session cost readout, and price table remote #36 — it compiles (not a type error) and throwsReferenceErrorat runtime, which would have taken the panel and the readout down with it. Implemented.UsageRemote.pricesand the namespace member are now optional, and a Host without the endpoint lands in a permanent "no prices" state instead of calling an undefined method.Tests
npm test— 447 passing (396 before, 51 added),npm run typecheckclean,npm run buildclean.The new tests pin behaviour rather than coverage:
tests/model-prices.test.tsfails 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.tspins 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.tspins 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.tspins the one-shot cache and the no-endpoint Host.tests/client-boot.test.tsnow asserts the realapply()seats all four surfaces and that thestatscell 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.