Skip to content

feat(desktop): NIP-AM agent-usage UI — bars, range tabs, focused view + cache breakdown - #4001

Open
wpfleger96 wants to merge 2 commits into
mainfrom
duncan/agent-usage-ui
Open

feat(desktop): NIP-AM agent-usage UI — bars, range tabs, focused view + cache breakdown#4001
wpfleger96 wants to merge 2 commits into
mainfrom
duncan/agent-usage-ui

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jul 31, 2026

Copy link
Copy Markdown
Member

What

Adds the full agent-usage UI feature: daily token-usage bars, 7/30-day range tabs, and a focused per-agent view, surfaced in agent profiles behind an owner-scoped visibility gate. The focused view displays total/input/output/cost plus a cache-token breakdown (cache-read, cache-write, fresh-input) in the totals card and per model. The NIP-AM backend it consumes merged in #4000.

Files

Area Files
Agent usage feature desktop/src/features/agent-usage/ — bars chart, range tabs, focused view, hooks, lib
Cache breakdown lib/agentUsage.ts (formatModelCacheBreakdown, hasKnownCacheData), AgentUsageFocusedView.tsx (Input-breakdown subsection + per-model line)
Profile panel UserProfilePanel, UserProfilePanelSections, UserProfilePanelTabs, UserProfilePanelUtils
Agents view AgentsScreen, AgentsView (reorder)
Context ProfilePanelContext
E2E bridge e2eBridge.ts, tests/helpers/bridge.ts (mock handler + fixture types)
E2E specs agent-usage.spec.ts (21 tests), agent-usage-screenshots.spec.ts (3 tests)
Config playwright.config.ts, package.json (recharts)

Cache-token display

The wire type (AgentUsageSeries) already carried cacheReadTokens / cacheWriteTokens / freshInputTokens at every scope (totals, per-day buckets, per-model); the focused view now renders them. An Input-breakdown subsection in the totals card shows the three subsets as TokenStats, gated on any known cache value, and each per-model row carries a compact breakdown line beneath it.

Fields follow the existing unknown-not-zero semantics: an absent subset is omitted from the per-model line and renders in the totals (never 0); a known-but-incomplete value shows its lower bound with a Partial marker; and a scope with no cache data at all shows no subsection. Rows that never reported cache tokens therefore surface Partial/ honestly rather than a fabricated zero.

Stack

Stack: agent-usage-backend #4000 (merged at 5e4c05f90b) → this PR

Review note

The base UI content was reviewed at 3d6c9c413 (PR #2035, 9/9/9). This PR is the UI half of that reviewed diff, restacked onto current main, with the cache-token display added on top.

The restack re-expressed the usage feature in the profile-panel structure that #5574 ("Refine channel settings and profile panels") introduced: main realigned agent profiles around shared ProfileIngressRow rows and a setView/setTab state machine. The usage ingress is now a ProfileIngressRow in the Info tab (matching the Activity/Diagnostics siblings), and the focused view mounts on the view === "usage" branch with setView("summary", { replace: true }) on ineligibility. The owner-scoped visibility gate (viewerIsOwner && isBot) is preserved exactly.

@wpfleger96
wpfleger96 force-pushed the duncan/agent-usage-backend branch from 1613f34 to 12f1f29 Compare July 31, 2026 20:53
@wpfleger96
wpfleger96 force-pushed the duncan/agent-usage-ui branch from b6491fc to 4173c78 Compare July 31, 2026 21:06
@wpfleger96
wpfleger96 force-pushed the duncan/agent-usage-backend branch from 12f1f29 to 635b030 Compare August 3, 2026 18:28
@wpfleger96
wpfleger96 force-pushed the duncan/agent-usage-ui branch from 4173c78 to 730583e Compare August 3, 2026 18:28
@wpfleger96
wpfleger96 force-pushed the duncan/agent-usage-backend branch from 635b030 to 39e1a91 Compare August 3, 2026 18:56
@wpfleger96
wpfleger96 force-pushed the duncan/agent-usage-ui branch 2 times, most recently from d0e14d3 to d27d77c Compare August 3, 2026 19:31
@wpfleger96
wpfleger96 marked this pull request as ready for review August 3, 2026 19:36
@wpfleger96
wpfleger96 requested a review from a team as a code owner August 3, 2026 19:36
@wpfleger96
wpfleger96 force-pushed the duncan/agent-usage-backend branch from 39e1a91 to 66a09a8 Compare August 4, 2026 01:03
@wpfleger96
wpfleger96 force-pushed the duncan/agent-usage-backend branch 9 times, most recently from eb24590 to 762e47b Compare August 8, 2026 21:43
wpfleger96 added a commit that referenced this pull request Aug 10, 2026
…hive + P4a aggregation/D6 (#4000)

## What

Implements Phases 2 and 4a of the Usage v2 plan (plan events
`d0268cd0`/`0e95b035`), extending the archive backend to emit,
transport, archive, and aggregate both cache categories and billing
identity fail-closed.

### P2 — emission, transport, archive

**Tri-state accumulators** (`Unseen`/`Exact`/`Unknown`) for cache-read
and cache-write in `buzz-agent` turn and session state. Absent field =
Unknown (never zero) through the full pipeline. No `unwrap_or(0)` on the
cache path. Both cache folds are gated on usage-bearing responses (same
gate as the total-state and identity folds) — a response with no usage
at all must not poison either accumulator.

**Overflow-aware input token parsing and accumulation** — closed
end-to-end from parse through wire to ACP:
- `sum_usage()` returns `SumUsageResult` (`Exact(u64)` | `Overflow`) —
checked arithmetic, never clamps. `anthropic_input_tokens()` returns
`Option<SumUsageResult>` since it sums three fields (`input_tokens +
cache_read_input_tokens + cache_creation_input_tokens`) that can
collectively overflow. Single-field callers (`prompt_tokens`,
`completion_tokens`, etc.) convert via `.into_exact()` — their
single-field sums cannot overflow.
- `LlmResponse.input_tokens_overflowed: bool` propagates the parse-layer
signal into the run loop. When set, `input_tokens` is `None` (clamped
value discarded), the context-gate baseline
(`last_request_input_tokens`) is frozen at its prior reading, and
`turn_input_tokens` is poisoned to `TurnIOState::Poisoned` before any
emission — including mid-turn `emit_usage_update` calls. A dedicated
enum on `LlmResponse.input_tokens` would ripple into ~20 existing test
assertions on `r.input_tokens == Some(...)`; the bool flag confines the
change to the two call sites that check it.
- `TurnIOState` (`Unseen`/`Exact`/`Poisoned`) for input and output:
per-round fold uses `checked_add`; overflow poisons permanently at turn
and session level, no healing. Absence does not poison (pass-2-cleared
contract unchanged). Wire emission omits
`accumulatedInputTokens`/`accumulatedOutputTokens` when poisoned — never
null, never `u64::MAX`. ACP treats absent = publisher-poisoned:
`delta_reliable: false`, null turn fields, null cumulative for that
category; session cumulative stays unknown for all subsequent turns once
poisoned.

**Conditional wire emission** for `accumulatedCachedInputTokens` and new
`accumulatedCacheWriteTokens`: fields are omitted when the cumulative is
Unseen or Unknown. ACP `_goose/unstable/session/update` contract
documented next to the payload with tests for all absence/zero variants.

**`PricingIdentity` stamping (publisher-side)**:
- `pricing_authority()`: canonical parsed-URL endpoint comparison
against the official allowlist — HTTPS only, exact allowlisted host
(lookalike-safe), default port (omitted or explicit :443), required API
base path, rejects userinfo/query/fragment/path-prefix lookalikes.
- Model: the actually-requested `request_model` after mesh/auto
resolution (not `effective_model_str`).
- Turn discipline: identity retained only while ALL usage in the current
turn carries one identical proven identity; any mismatch,
unproven-usage-bearing response, or unpaired cumulative snapshot poisons
to absent; a later matching notification does not heal a mixed turn.

**ACP `UsageTracker` identity fold**: per-in-flight-turn tri-state
identity accumulator replacing last-update-wins. Any absent identity on
a token-advancing notification or exact mismatch poisons to absent;
poison survives later updates; reset in `begin_turn()`/`take()`; reset
also when a request fails (baseline cleared so preflight gate cannot
stay frozen sub-threshold on retries).

**M3 migration**: adds `turn_cache_write_tokens`,
`cumulative_cache_write_tokens`, `pricing_authority`, `pricing_model`,
`pricing_cache_class` to `agent_metric_index`. Additive, idempotent,
guarded per-column by marker. M2 migration also guarded per-column (turn
and cumulative cache-read columns checked and added independently;
marker commits only after both are present). Fresh-DB schema includes
all columns.

**First-turn baselines**: `seed_zero_baseline` seeds `last_input:
Some(0)`, `last_output: Some(0)`, `last_cached_input: Some(0)`,
`last_cache_write: Some(0)`, and `last_total: Some(0)` — all have the
known-zero-at-spawn argument. Absent fields from incoming snapshots
still produce unknown (tri-state unchanged). Sessions buzz-acp did not
spawn (no seed) remain fail-closed on turn one.

**`ReportedUsage` TS mirror**: `cacheReadTokens`, `cacheWriteTokens`,
`freshInputTokens` added to `tauriArchive.ts` as `UsageField` members,
field-for-field with the Rust struct.

### P4a — aggregation layer

**Extended S-1 ladder** to cache-read and cache-write via the same
`ladder_token` path as the existing token fields.

**`freshInputTokens` derivation**: checked arithmetic, fail-closed —
absent cache fields produce Unknown (not zero), overflow and
`cacheRead+cacheWrite > input` both produce `incomplete: true`.
Aggregated as a `UsageField`.

**D6 comparator**: `sort_value()` = provider total when known, else
`input+output` when both known, else `None` (unknown-last). Replaces the
prior total-only comparator for both agent-level and model-level sort.
Ships a pinned test vector that the TS render layer (P5) must match.

## Test coverage

- `buzz-agent`: 440 lib + 15 integration (golden_transcripts) — includes
13 new `cache_total_state_tests`; 14 new `turn_io_state_tests`; 3 new
`sum_usage_*` tests (exact single-field, exact two-field, overflow
signals correctly); 3 new `parse_anthropic_*` tests (overflow flag set +
value cleared, normal sum no flag, absent usage no flag); end-to-end
golden transcript drives real subprocess with Anthropic-shaped
`input_tokens: u64::MAX, cache_read: 1` response and asserts
`accumulatedInputTokens` absent from the emitted `usage_update` — no
logic duplication; 3 wire pin tests; 4 `fold_pricing_identity_*` tests;
`pricing_authority()` explicit-:443 acceptance
- `buzz-acp`: 700 tests (691 lib + 9 integration) — 4 new usage tests
(absent input → unreliable+null; absent output → unreliable+null;
goose-shaped both present unchanged; poison mid-session); 3 ACP behavior
tests; 7 pool lifecycle tests
- Desktop (Rust): 2259+ tests — 14 new P4a pinned tests; 2 M3 round-trip
tests; 1 serde key-shape test; 2 M2 partial-schema migration tests;
first-turn cache round-trip test

## Related PRs

- P1 NIP-AM spec: [#4632](#4632)
- P3 pricing table: [#4629](#4629)
- UI (P5): [#4001](#4001)

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Base automatically changed from duncan/agent-usage-backend to main August 10, 2026 14:47
@wpfleger96
wpfleger96 force-pushed the duncan/agent-usage-ui branch from d27d77c to d71c2a0 Compare August 10, 2026 16:20
@wpfleger96
wpfleger96 force-pushed the duncan/agent-usage-ui branch from d71c2a0 to 01ab4be Compare August 12, 2026 21:31
Adds the agent-usage UI feature on top of the merged NIP-AM backend.

- `desktop/src/features/agent-usage/`: daily bars chart, 7/30-day range
  tabs, focused agent view with caveats, hooks, and lib utilities. All
  state is driven by the `get_agent_usage_series` Tauri command.
- Profile panel: usage ingress row + focused view wired into agent
  profiles; gated on owner-scoped visibility (`viewerIsOwner && isBot`).
- `AgentsView`: agent-usage overview section.
- `AgentsScreen`/`ProfilePanelContext`: usage navigation state.
- `e2eBridge.ts` + `tests/helpers/bridge.ts`: `get_agent_usage_series`
  mock handler + fixture types, including the M2 cache fields
  (`cacheReadTokens`, `cacheWriteTokens`, `freshInputTokens`) the wire
  type carries. The UI absorbs but does not display these fields.
- `tests/e2e/agent-usage.spec.ts` + `agent-usage-screenshots.spec.ts`:
  Playwright coverage for loading, empty state, bars, range switching,
  error/retry, focused view, caveats, and cache invalidation.
- `desktop/playwright.config.ts`: agent-usage specs added to suite.
- `desktop/package.json`: recharts dependency.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 force-pushed the duncan/agent-usage-ui branch from 01ab4be to 3d723a7 Compare August 12, 2026 21:34
…t-usage focused view

The plumbing already returned cacheReadTokens/cacheWriteTokens/freshInputTokens
at every scope of AgentUsageSeries, but the focused view rendered only the four
top-level stats and never surfaced them. Add an Input-breakdown subsection to the
totals card (Cache read / Cache write / Fresh input) gated on any known cache
value, and a compact per-model breakdown line. Fields follow the existing
unknown-not-zero semantics: absent subsets are omitted, a known-but-incomplete
value shows its lower bound with a Partial marker, and a scope with no cache data
renders no subsection at all.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 changed the title feat(desktop): NIP-AM agent-usage UI — bars, range tabs, focused view feat(desktop): NIP-AM agent-usage UI — bars, range tabs, focused view + cache breakdown Aug 12, 2026
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.

1 participant