feat(usage): desktop usage summary surface (Phase 1 of #77221) - #77251
feat(usage): desktop usage summary surface (Phase 1 of #77221)#77251tneemo wants to merge 4 commits into
Conversation
…7221) Add a usage.summary JSON-RPC method (tui_gateway/methods_config.py) that aggregates the existing local metering tables (sessions + session_model_usage) into a cheap summary: session counts, token totals (input/output/cache-read), and estimated spend, degrading gracefully to zeros when the DB is unavailable. Add a bundled desktop 'Usage' plugin (apps/desktop/src/plugins/usage) with summary cards rendered via host.request('usage.summary'), registered through ROUTES_AREA + SIDEBAR_NAV_AREA (auto-discovered by the plugins glob). Charts/heatmap are deliberately Phase 2. Co-authored-by: codex (gpt-5.6-luna)
There was a problem hiding this comment.
Pull request overview
Adds an initial “Usage” surface to the Hermes Desktop app by exposing a lightweight local-DB usage aggregate over JSON-RPC and rendering it as a new plugin page. This fits the existing desktop plugin architecture (route + sidebar contribution) and reuses the existing state.db metering tables rather than introducing a new metering path.
Changes:
- Adds a new
usage.summaryJSON-RPC handler that aggregates session/token/cost totals fromstate.db. - Introduces a bundled Desktop plugin that registers
/usage+ sidebar nav and renders summary cards fromusage.summary. - Implements a basic UI layout with loading/error states via the plugin SDK.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| tui_gateway/methods_config.py | Adds usage.summary RPC that aggregates metering totals from sessions / session_model_usage. |
| apps/desktop/src/plugins/usage/plugin.tsx | Registers the “Usage” route and sidebar navigation entry via the plugin SDK. |
| apps/desktop/src/plugins/usage/page.tsx | Fetches usage.summary and renders summary cards (sessions, tokens, costs). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| by_session = db._conn.execute( | ||
| """ | ||
| SELECT COALESCE(SUM(estimated_cost_usd), 0) AS estimated_cost_usd | ||
| FROM session_model_usage | ||
| GROUP BY session_id | ||
| """ | ||
| ).fetchall() | ||
| payload.update( | ||
| total_estimated_cost_usd=float(usage["estimated_cost_usd"] or 0.0), | ||
| total_input_tokens=int(usage["input_tokens"] or 0), | ||
| total_output_tokens=int(usage["output_tokens"] or 0), | ||
| total_cache_read_tokens=int(usage["cache_read_tokens"] or 0), | ||
| most_expensive_session_usd=max( | ||
| (float(row["estimated_cost_usd"] or 0.0) for row in by_session), | ||
| default=0.0, | ||
| ), | ||
| ) |
| const formatUsd = (value: number) => | ||
| new Intl.NumberFormat(undefined, { currency: 'USD', style: 'currency', minimumFractionDigits: 2 }).format(value || 0) |
| SUM(CASE WHEN COALESCE(input_tokens, 0) > 0 | ||
| OR COALESCE(output_tokens, 0) > 0 | ||
| OR COALESCE(cache_read_tokens, 0) > 0 | ||
| OR COALESCE(cache_write_tokens, 0) > 0 | ||
| THEN 1 ELSE 0 END) AS token_sessions, |
Withdrawing this draft in favor of #77537 / #77572Closing this Phase-1 draft. The same surface (usage summary cards) plus the full Phase-2 scope (daily sparkline, per-model/provider breakdown) is implemented in #77537 and #77572 by reusing the existing The branch remains on the fork if anyone wants the |
- Align token categories: add total_cache_write_tokens so the token-session count (which includes cache-write activity) matches a displayed total. - Preserve micro-cost precision: formatUsd uses 6 decimals for values in (0, 0.01) instead of collapsing to $0.00 (e.g. cheapest session $0.000056). - Derive both cost extrema from the same per-session aggregate (filtered to positive costs) so cheapest/most-expensive tell one story.
Reopened — this PR follows the issue's specified architecture (draft)I closed this earlier thinking the newer PRs (#77537 / #77572) superseded it. Re-examining the #77221 spec, this PR is the one that implements the architecture the issue explicitly asks for:
This PR: new #77537/#77572: register via Review findings addressed (pushed
|
The RPC already returned total_cache_write_tokens but the page's total-token calculation omitted it — making the displayed total disagree with the token-session count (review finding). Include it.
- Pin formatUsd to the en-US locale (Intl.NumberFormat('en-US', ...))
instead of the ambient locale, so USD rendering is consistent
regardless of the system language.
- Compute cost extrema directly in SQL (MAX/MIN over the per-session
aggregate subquery) instead of fetchall() + Python min/max.
Addressed: Copilot's locale + SQL-extrema suggestions (pushed
|
Phase 1: local token/cost usage summary in the Desktop app
Closes #77221 (Phase 1 — summary surface; charts/heatmap are deliberately Phase 2).
Backend —
usage.summaryJSON-RPC methodtui_gateway/methods_config.pyadds ausage.summaryhandler that aggregates the existing local metering tables (sessions+session_model_usage) — no parallel metering system:total_sessions,token_sessions,cost_sessionstotal_estimated_cost_usd,most_expensive_session_usd,cheapest_session_usdtotal_input_tokens,total_output_tokens,total_cache_read_tokensDegrades gracefully to zeros when the DB is unavailable (matches the other handlers' fail-soft pattern). Session-level columns are used as a compatibility fallback for older DBs that lack
session_model_usage.Frontend — bundled "Usage" plugin
apps/desktop/src/plugins/usage/adds a bundled plugin (auto-discovered by the existingimport.meta.glob('../plugins/*/plugin.{ts,tsx}')):plugin.tsx— registersROUTES_AREA(/usage) +SIDEBAR_NAV_AREA("Usage", codicongraph-line)page.tsx— callshost.request('usage.summary')on mount and renders summary cards (sessions, spend USD, tokens, most/least expensive session), using the plugin SDK'sLoader/ErrorStateand the app's dark-theme CSS variablesVerification
python -c "import ast; ast.parse(open('tui_gateway/methods_config.py').read())"— cleannpx tsc --noEmit(apps/desktop) — cleannpx vitest run src/contrib/ src/plugins/— 13/13 passstate.db: 12 sessions, 9 token-bearing, 491M cache-read tokens aggregated correctlycheapest_session_usdnow ignores zero-cost sessions (MIN(CASE WHEN estimated_cost_usd > 0 ...)) instead of returning 0Phase 2 (not in this PR)
GitHub-style cost/token heatmap, cumulative-cost line graph, per-model/provider breakdowns, daily/weekly/monthly views — per the issue's full spec.