diff --git a/desktop/src/features/agent-usage/lib/agentUsage.test.mjs b/desktop/src/features/agent-usage/lib/agentUsage.test.mjs index c83d55758..7781a148e 100644 --- a/desktop/src/features/agent-usage/lib/agentUsage.test.mjs +++ b/desktop/src/features/agent-usage/lib/agentUsage.test.mjs @@ -4,7 +4,7 @@ import test from "node:test"; import { bigintRatio, buildLocalDayBoundaries, - deriveApproxTotal, + deriveDisplayTotal, deriveUsageIngressTrailing, formatCoverageDate, formatEstimatedCostUsd, @@ -14,8 +14,8 @@ import { isUnknownField, msUntilNextLocalMidnight, parseTokenCount, - sortAgentsByKnownTotal, - sortModelsByKnownTotal, + sortAgentsByDisplayTotal, + sortModelsByDisplayTotal, sumKnownBucketTotals, } from "./agentUsage.ts"; @@ -334,122 +334,214 @@ test("bigintRatio clamps part to [0, whole]", () => { assert.equal(bigintRatio(200n, 100n), 1); }); -// ── deriveApproxTotal ───────────────────────────────────────────────────────── +// ── deriveDisplayTotal ──────────────────────────────────────────────────────── -test("deriveApproxTotal returns null when genuine total is known (no approximation needed)", () => { +test("deriveDisplayTotal returns exact kind when totalTokens is present", () => { const usage = reportedUsage({ inputTokens: usageField({ value: "800" }), outputTokens: usageField({ value: "200" }), totalTokens: usageField({ value: "1100" }), }); - assert.equal(deriveApproxTotal(usage), null); + const dt = deriveDisplayTotal(usage); + assert.equal(dt.kind, "exact"); + assert.equal(dt.value, 1100n); + assert.equal(dt.partial, false); }); -test("deriveApproxTotal sums input and output when total is null", () => { +test("deriveDisplayTotal carries partial=true for an exact total flagged incomplete", () => { + const usage = reportedUsage({ + totalTokens: usageField({ value: "900", incomplete: true }), + }); + const dt = deriveDisplayTotal(usage); + assert.equal(dt.kind, "exact"); + assert.equal(dt.value, 900n); + assert.equal(dt.partial, true); +}); + +test("deriveDisplayTotal returns approximate kind when totalTokens is null but i/o is known", () => { const usage = reportedUsage({ inputTokens: usageField({ value: "800" }), outputTokens: usageField({ value: "200" }), }); - assert.equal(deriveApproxTotal(usage), 1000n); + const dt = deriveDisplayTotal(usage); + assert.equal(dt.kind, "approximate"); + assert.equal(dt.value, 1000n); + assert.equal(dt.partial, false); }); -test("deriveApproxTotal returns input alone when output is null", () => { +test("deriveDisplayTotal approximate partial=true when either i/o field is incomplete", () => { + const usage = reportedUsage({ + inputTokens: usageField({ value: "800", incomplete: true }), + outputTokens: usageField({ value: "200" }), + }); + const dt = deriveDisplayTotal(usage); + assert.equal(dt.kind, "approximate"); + assert.equal(dt.partial, true); +}); + +test("deriveDisplayTotal returns approximate from input alone when output is null", () => { const usage = reportedUsage({ inputTokens: usageField({ value: "500" }), }); - assert.equal(deriveApproxTotal(usage), 500n); + const dt = deriveDisplayTotal(usage); + assert.equal(dt.kind, "approximate"); + assert.equal(dt.value, 500n); }); -test("deriveApproxTotal returns output alone when input is null", () => { +test("deriveDisplayTotal returns approximate from output alone when input is null", () => { const usage = reportedUsage({ outputTokens: usageField({ value: "300" }), }); - assert.equal(deriveApproxTotal(usage), 300n); + const dt = deriveDisplayTotal(usage); + assert.equal(dt.kind, "approximate"); + assert.equal(dt.value, 300n); }); -test("deriveApproxTotal returns null when total, input, and output are all null", () => { +test("deriveDisplayTotal returns unknown kind when all fields are null", () => { const usage = reportedUsage(); - assert.equal(deriveApproxTotal(usage), null); + const dt = deriveDisplayTotal(usage); + assert.equal(dt.kind, "unknown"); + assert.equal(dt.value, null); + assert.equal(dt.partial, false); }); -// ── sortAgentsByKnownTotal / sortModelsByKnownTotal ───────────────────────── +// ── sortAgentsByDisplayTotal / sortModelsByDisplayTotal ───────────────────── -test("sortAgentsByKnownTotal ranks known totals descending", () => { +test("sortAgentsByDisplayTotal ranks known exact totals descending", () => { const agents = [ agentUsage("a1", "100"), agentUsage("a2", "300"), agentUsage("a3", "200"), ]; - const sorted = sortAgentsByKnownTotal(agents); + const sorted = sortAgentsByDisplayTotal(agents); assert.deepEqual( sorted.map((a) => a.agentPubkey), ["a2", "a3", "a1"], ); }); -test("sortAgentsByKnownTotal lists unknown-total agents after all known-total agents, never interleaved", () => { +test("sortAgentsByDisplayTotal ranks exact totals above approximate totals", () => { + const exactAgent = agentUsage("exact", "50"); + const approxAgent = agentUsage("approx", null, { + usage: reportedUsage({ + inputTokens: usageField({ value: "9000" }), + outputTokens: usageField({ value: "9000" }), + }), + }); + const sorted = sortAgentsByDisplayTotal([approxAgent, exactAgent]); + // exact(50) < approximate(18000) numerically, but exact tier wins + assert.equal(sorted[0].agentPubkey, "exact"); + assert.equal(sorted[1].agentPubkey, "approx"); +}); + +test("sortAgentsByDisplayTotal ranks approximate totals above unknown totals", () => { + const approxAgent = agentUsage("approx", null, { + usage: reportedUsage({ + inputTokens: usageField({ value: "100" }), + }), + }); + const unknownAgent = agentUsage("unknown", null); + const sorted = sortAgentsByDisplayTotal([unknownAgent, approxAgent]); + assert.equal(sorted[0].agentPubkey, "approx"); + assert.equal(sorted[1].agentPubkey, "unknown"); +}); + +test("sortAgentsByDisplayTotal handles mixed exact/approximate/unknown population in tier order", () => { + const agents = [ + agentUsage("u1", null), // unknown + agentUsage("e1", "100"), // exact + agentUsage("a1", null, { // approximate + usage: reportedUsage({ inputTokens: usageField({ value: "500" }) }), + }), + agentUsage("u2", null), // unknown + agentUsage("e2", "300"), // exact + agentUsage("a2", null, { // approximate + usage: reportedUsage({ inputTokens: usageField({ value: "200" }) }), + }), + ]; + const sorted = sortAgentsByDisplayTotal(agents); + // Tier order: exact first (e2=300 > e1=100), then approx (a1=500 > a2=200), then unknown (u1 < u2 by pubkey) + assert.deepEqual( + sorted.map((a) => a.agentPubkey), + ["e2", "e1", "a1", "a2", "u1", "u2"], + ); +}); + +test("sortAgentsByDisplayTotal lists unknown-total agents after all other agents, tiebroken by pubkey", () => { const agents = [ agentUsage("unknown-b", null), agentUsage("known", "50"), agentUsage("unknown-a", null), ]; - const sorted = sortAgentsByKnownTotal(agents); + const sorted = sortAgentsByDisplayTotal(agents); assert.equal(sorted[0].agentPubkey, "known"); - // Unknown-total agents tiebreak by normalized pubkey. assert.deepEqual( sorted.slice(1).map((a) => a.agentPubkey), ["unknown-a", "unknown-b"], ); }); -test("sortAgentsByKnownTotal tiebreaks equal known totals by pubkey", () => { +test("sortAgentsByDisplayTotal tiebreaks equal exact totals by pubkey", () => { const agents = [agentUsage("b", "100"), agentUsage("a", "100")]; - const sorted = sortAgentsByKnownTotal(agents); + const sorted = sortAgentsByDisplayTotal(agents); assert.deepEqual( sorted.map((a) => a.agentPubkey), ["a", "b"], ); }); -test("sortModelsByKnownTotal sorts null model ('Unknown model') last among ties", () => { +test("sortModelsByDisplayTotal sorts null model ('Unknown model') last among ties", () => { const models = [ modelUsage(null, "100"), modelUsage("gpt-4", "100"), modelUsage("claude", "100"), ]; - const sorted = sortModelsByKnownTotal(models); + const sorted = sortModelsByDisplayTotal(models); assert.deepEqual( sorted.map((m) => m.model), ["claude", "gpt-4", null], ); }); -test("sortModelsByKnownTotal tiebreaks harness before model when totals are equal", () => { +test("sortModelsByDisplayTotal tiebreaks harness before model when totals are equal", () => { const models = [ modelUsage("m", "100", { harness: "z-harness" }), modelUsage("m", "100", { harness: "a-harness" }), modelUsage("m", "100", { harness: null }), ]; - const sorted = sortModelsByKnownTotal(models); + const sorted = sortModelsByDisplayTotal(models); assert.deepEqual( sorted.map((m) => m.harness), ["a-harness", "z-harness", null], ); }); -test("sortModelsByKnownTotal same model two harnesses produces two rows in harness order", () => { +test("sortModelsByDisplayTotal same model two harnesses produces two rows in harness order", () => { // Same model via two harnesses should be distinct rows; harness-ascending tiebreak. const models = [ modelUsage("claude-sonnet", "500", { harness: "goose" }), modelUsage("claude-sonnet", "500", { harness: "claude-code" }), ]; - const sorted = sortModelsByKnownTotal(models); + const sorted = sortModelsByDisplayTotal(models); assert.deepEqual( sorted.map((m) => m.harness), ["claude-code", "goose"], ); }); +test("sortModelsByDisplayTotal ranks exact tier above approximate tier regardless of value", () => { + const exactModel = modelUsage("small-model", "10"); + const approxModel = modelUsage("big-approx", null, { + usage: reportedUsage({ + inputTokens: usageField({ value: "9999" }), + outputTokens: usageField({ value: "9999" }), + }), + }); + const sorted = sortModelsByDisplayTotal([approxModel, exactModel]); + assert.equal(sorted[0].model, "small-model"); // exact tier wins + assert.equal(sorted[1].model, "big-approx"); +}); + // ── isPartialField / isUnknownField ────────────────────────────────────────── test("isPartialField is true only for a known value flagged incomplete", () => { diff --git a/desktop/src/features/agent-usage/lib/agentUsage.ts b/desktop/src/features/agent-usage/lib/agentUsage.ts index 2b9cbdb9a..d28ddf7dd 100644 --- a/desktop/src/features/agent-usage/lib/agentUsage.ts +++ b/desktop/src/features/agent-usage/lib/agentUsage.ts @@ -189,85 +189,119 @@ export function bigintRatio(part: bigint, whole: bigint): number { return Number(permille) / 1000; } -// ── Display approximation (A2 presentation layer) ──────────────────────────── +// ── Display total derivation (A2 presentation layer) ───────────────────────── /** - * Derive a display approximation of the total by summing known input and output - * token counts. Returns `null` if neither field is known. This is a *display - * label* only — it is NEVER written to the wire or stored; NIP-AM's - * "MUST NOT derive total = input + output" governs published/stored data only. - * Callers MUST prefix the result with `≈` so the approximation is honest. + * A provenance-bearing display total for the usage UI. Only one of three + * states is ever active: + * + * - `exact`: `totalTokens.value` is present and parsed. `partial` mirrors the + * wire field's `incomplete` flag. + * - `approximate`: `totalTokens.value` is absent but at least one of + * `inputTokens` / `outputTokens` is known; `value` is their bigint-safe sum. + * `partial` is `inputTokens.incomplete || outputTokens.incomplete`. + * Callers MUST render `≈` to distinguish this from a provider total. + * - `unknown`: no token counts are available at all; `value` is `null`. + * + * This is a *display* value only — it is NEVER written to the wire or stored. + * NIP-AM's "MUST NOT derive total = input + output" governs published/stored + * data; this label lives entirely in the presentation layer. */ -export function deriveApproxTotal(usage: { +export type DisplayTotal = + | { kind: "exact"; value: bigint; partial: boolean } + | { kind: "approximate"; value: bigint; partial: boolean } + | { kind: "unknown"; value: null; partial: false }; + +export function deriveDisplayTotal(usage: { inputTokens: UsageField; outputTokens: UsageField; totalTokens: UsageField; -}): bigint | null { - if (parseTokenCount(usage.totalTokens.value) !== null) { - // Genuine total is known — callers should use it directly; no approximation needed. - return null; +}): DisplayTotal { + const exact = parseTokenCount(usage.totalTokens.value); + if (exact !== null) { + return { kind: "exact", value: exact, partial: isPartialField(usage.totalTokens) }; } const input = parseTokenCount(usage.inputTokens.value); const output = parseTokenCount(usage.outputTokens.value); - if (input === null && output === null) return null; - return (input ?? 0n) + (output ?? 0n); + if (input !== null || output !== null) { + return { + kind: "approximate", + value: (input ?? 0n) + (output ?? 0n), + partial: isPartialField(usage.inputTokens) || isPartialField(usage.outputTokens), + }; + } + return { kind: "unknown", value: null, partial: false }; } -// ── Ranking (A2: known lower-bound totals rank; null totals list after) ───── +// ── Ranking (A2: rank by display total — exact > approximate > unknown) ────── -type Ranked = { item: T; totalTokens: bigint | null }; +type DisplayTierKey = 0 | 1 | 2; // 0 = exact, 1 = approximate, 2 = unknown -function rankByKnownTotal( +type RankedWithDisplay = { + item: T; + displayTotal: DisplayTotal; + tierKey: DisplayTierKey; +}; + +function tierOf(dt: DisplayTotal): DisplayTierKey { + if (dt.kind === "exact") return 0; + if (dt.kind === "approximate") return 1; + return 2; +} + +/** + * Sort items by their display total: + * 1. Exact totals rank first, descending by value. + * 2. Approximate totals (≈ in+out) rank next, descending by value. + * 3. Unknown totals rank last, unordered beyond the tiebreak. + * Within the same tier and value, `tiebreak` resolves the order. + */ +function rankByDisplayTotal( items: readonly T[], - totalTokens: (item: T) => UsageField, + getUsage: (item: T) => { inputTokens: UsageField; outputTokens: UsageField; totalTokens: UsageField }, tiebreak: (a: T, b: T) => number, ): T[] { - const withTotals: Ranked[] = items.map((item) => ({ - item, - totalTokens: parseTokenCount(totalTokens(item).value), - })); + const withDisplay: RankedWithDisplay[] = items.map((item) => { + const dt = deriveDisplayTotal(getUsage(item)); + return { item, displayTotal: dt, tierKey: tierOf(dt) }; + }); - return withTotals + return withDisplay .sort((a, b) => { - if (a.totalTokens !== null && b.totalTokens !== null) { - if (a.totalTokens !== b.totalTokens) { - return a.totalTokens > b.totalTokens ? -1 : 1; + if (a.tierKey !== b.tierKey) return a.tierKey - b.tierKey; + // Same tier — for exact/approximate, sort descending by value. + if (a.displayTotal.value !== null && b.displayTotal.value !== null) { + if (a.displayTotal.value !== b.displayTotal.value) { + return a.displayTotal.value > b.displayTotal.value ? -1 : 1; } - return tiebreak(a.item, b.item); } - // Known-total rows rank before unknown-total rows; never interleave. - if (a.totalTokens !== null) return -1; - if (b.totalTokens !== null) return 1; return tiebreak(a.item, b.item); }) .map((ranked) => ranked.item); } -/** Agents sort by known `totalTokens` descending, then normalized pubkey (A2/plan). Unknown-total agents list after all known-total agents, unranked among themselves beyond the pubkey tiebreak. */ -export function sortAgentsByKnownTotal( +/** Agents sort by display total (exact → approximate → unknown), descending by value within tier, then normalized pubkey. */ +export function sortAgentsByDisplayTotal( agents: readonly AgentUsage[], ): AgentUsage[] { - return rankByKnownTotal( + return rankByDisplayTotal( agents, - (agent) => agent.usage.totalTokens, + (agent) => agent.usage, (a, b) => a.agentPubkey.localeCompare(b.agentPubkey), ); } -/** Model rows use the same ranking rule as agents, tiebroken by harness name +/** Model rows use the same display-total ranking, tiebroken by harness name * (null harness sorts last), then by model name (null model sorts last). * Ordinal (`<`/`>`) comparators are used so ordering is locale-independent - * and matches the Rust backend's `String::cmp` byte order. - * Note: harness/model identifiers are ASCII in practice; UTF-16 vs UTF-8 - * scalar divergence for astral code points is accepted and not a use case. */ -export function sortModelsByKnownTotal( + * and matches the Rust backend's `String::cmp` byte order. */ +export function sortModelsByDisplayTotal( models: readonly AgentUsageModel[], ): AgentUsageModel[] { - return rankByKnownTotal( + return rankByDisplayTotal( models, - (model) => model.usage.totalTokens, + (model) => model.usage, (a, b) => { - // Harness tiebreak first (ordinal, None last). const harnessCmp = a.harness === b.harness ? 0 @@ -279,7 +313,6 @@ export function sortModelsByKnownTotal( ? -1 : 1; if (harnessCmp !== 0) return harnessCmp; - // Then model (ordinal, None last). if (a.model === b.model) return 0; if (a.model === null) return 1; if (b.model === null) return -1; @@ -370,9 +403,9 @@ export function sumKnownBucketTotals( partial = true; } if (known === null) { - const approx = deriveApproxTotal(bucket.usage); - if (approx !== null) { - approxSum += approx; + const dt = deriveDisplayTotal(bucket.usage); + if (dt.kind === "approximate") { + approxSum += dt.value; sawApprox = true; } } diff --git a/desktop/src/features/agent-usage/ui/AgentUsageDailyBars.tsx b/desktop/src/features/agent-usage/ui/AgentUsageDailyBars.tsx index da02c2db1..3003dd3f5 100644 --- a/desktop/src/features/agent-usage/ui/AgentUsageDailyBars.tsx +++ b/desktop/src/features/agent-usage/ui/AgentUsageDailyBars.tsx @@ -4,7 +4,7 @@ import { cn } from "@/shared/lib/cn"; import type { AgentUsageSeriesBucket } from "@/shared/api/tauriArchive"; import { bigintRatio, - deriveApproxTotal, + deriveDisplayTotal, formatTokenCountCompact, isPartialField, parseTokenCount, @@ -66,13 +66,13 @@ function deriveBarState(bucket: AgentUsageSeriesBucket) { knownTokens: known, }; } - // Genuine total unknown — try i/o approximation before falling back to hatched. - const approx = deriveApproxTotal(bucket.usage); - if (approx !== null) { + // Genuine total unknown — derive the display total for the bar. + const dt = deriveDisplayTotal(bucket.usage); + if (dt.kind === "approximate") { return { - accessibleLabel: `${dateLabel} · ≈ ${formatTokenCountCompact(approx)} tokens (approx)`, + accessibleLabel: `${dateLabel} · ≈ ${formatTokenCountCompact(dt.value)} tokens (approx)`, kind: "approx" as const, - knownTokens: approx, + knownTokens: dt.value, }; } return { @@ -101,10 +101,10 @@ export function AgentUsageDailyBars({ buckets.reduce((max, bucket) => { const total = parseTokenCount(bucket.usage.totalTokens.value); if (total !== null) return total > max ? total : max; - // Fall back to the i/o approximation so bars scale correctly when - // no bucket reports a genuine total. - const approx = deriveApproxTotal(bucket.usage); - return approx !== null && approx > max ? approx : max; + // Fall back to the display total's approximate value so bars scale + // correctly when no bucket reports a genuine total. + const dt = deriveDisplayTotal(bucket.usage); + return dt.kind === "approximate" && dt.value > max ? dt.value : max; }, 0n), [buckets], ); diff --git a/desktop/src/features/agent-usage/ui/AgentUsageFocusedView.tsx b/desktop/src/features/agent-usage/ui/AgentUsageFocusedView.tsx index b1f5aa751..a474d6ee0 100644 --- a/desktop/src/features/agent-usage/ui/AgentUsageFocusedView.tsx +++ b/desktop/src/features/agent-usage/ui/AgentUsageFocusedView.tsx @@ -14,15 +14,16 @@ import { Skeleton } from "@/shared/ui/skeleton"; import { Tabs, TabsList, TabsTrigger } from "@/shared/ui/tabs"; import { useAgentUsageSeries } from "../hooks"; import { + deriveDisplayTotal, formatCoverageDate, formatEstimatedCostUsd, formatTokenCountCompact, formatTokenCountExact, - deriveApproxTotal, isPartialField, isUnknownField, parseTokenCount, - sortModelsByKnownTotal, + sortModelsByDisplayTotal, + type DisplayTotal, type UsageWindowDays, } from "../lib/agentUsage"; import { AgentUsageDailyBars } from "./AgentUsageDailyBars"; @@ -204,28 +205,29 @@ function AgentUsageFocusedTotals({ agent: AgentUsageSeries["agents"][number]; coverage: AgentUsageSeries["coverage"]; }) { - const { estimatedCostUsd, inputTokens, outputTokens, totalTokens } = + const { estimatedCostUsd, inputTokens, outputTokens } = agent.usage; - const models = sortModelsByKnownTotal(agent.models); - // Show the caveat paragraph only when there are genuinely invalid/excluded - // rows or truly unknown i/o deltas. `agent.hasUnknownUsage` is true when - // deltaReliable is false (some intervals couldn't be computed). The - // `coverage.invalidReportCount > 0` path covers rows excluded from buckets. - // We do NOT trigger on totalTokens.value being null: that's the permanent - // state for all real publishers today (no harness emits a total yet), and - // showing the caveat permanently would make it read as a persistent error. - const explainPartial = - agent.hasUnknownUsage || coverage.invalidReportCount > 0; + const models = sortModelsByDisplayTotal(agent.models); + // `explainPartial` controls the caveat paragraph. Each sentence is gated + // only on the condition that proves it: + // - unknown-intervals sentence: `agent.hasUnknownUsage` — true when + // deltaReliable is false for at least one interval in the window. + // - invalid-reports sentence: `coverage.invalidReportCount > 0` — true + // when rows were excluded from buckets due to bad timestamps or missing + // session cumulative totals. + // We do NOT trigger on totalTokens.value being null — that's the permanent + // state for all real publishers today, not a data quality problem. + const showUnknownIntervalsCaveat = agent.hasUnknownUsage; + const showInvalidReportsCaveat = coverage.invalidReportCount > 0; - // Approximation for the Total tokens stat when no genuine total is available. - const approxTotal = deriveApproxTotal(agent.usage); + // Display total for the Total tokens stat. + const displayTotal = deriveDisplayTotal(agent.usage); return (
@@ -300,11 +302,19 @@ function AgentUsageFocusedTotals({ {" · "} {formatCoverageRange(coverage)}

- {explainPartial ? ( -

- Some usage could not be counted: reports with an unreadable - timestamp or a cumulative total missing its session are excluded, - and unknown intervals are omitted rather than shown as zero. + {showUnknownIntervalsCaveat ? ( +

+ Some usage could not be counted: unknown intervals are omitted + rather than shown as zero. +

+ ) : null} + {showInvalidReportsCaveat ? ( +

+ {coverage.invalidReportCount === 1 + ? "1 report" + : `${coverage.invalidReportCount} reports`}{" "} + excluded: reports with an unreadable timestamp or a cumulative + total missing its session are not assigned to any day.

) : null}
@@ -335,23 +345,20 @@ function TokenStat({ * honest without hiding that real token activity was counted. */ function ApproxTokenStat({ - approxTotal, - field, + displayTotal, label, }: { - approxTotal: bigint | null; - field: { value: string | null; incomplete: boolean }; + displayTotal: DisplayTotal; label: string; }) { - const parsed = parseTokenCount(field.value); const display = - parsed !== null - ? formatTokenCountExact(parsed) - : approxTotal !== null - ? `≈ ${formatTokenCountExact(approxTotal)}` + displayTotal.kind === "exact" + ? formatTokenCountExact(displayTotal.value) + : displayTotal.kind === "approximate" + ? `≈ ${formatTokenCountExact(displayTotal.value)}` : null; return ( - + ); } diff --git a/desktop/src/features/agent-usage/ui/AgentUsageSection.tsx b/desktop/src/features/agent-usage/ui/AgentUsageSection.tsx index 95d4a8f11..dada3f269 100644 --- a/desktop/src/features/agent-usage/ui/AgentUsageSection.tsx +++ b/desktop/src/features/agent-usage/ui/AgentUsageSection.tsx @@ -21,13 +21,10 @@ import { Tabs, TabsList, TabsTrigger } from "@/shared/ui/tabs"; import { useAgentUsageSeries } from "../hooks"; import { bigintRatio, - deriveApproxTotal, + deriveDisplayTotal, formatCoverageDate, formatTokenCountCompact, - isPartialField, - isUnknownField, - parseTokenCount, - sortAgentsByKnownTotal, + sortAgentsByDisplayTotal, sumKnownBucketTotals, type UsageWindowDays, } from "../lib/agentUsage"; @@ -51,7 +48,7 @@ export function AgentUsageSection({ const { onOpenSettings } = useAppShell(); const agents = React.useMemo( - () => sortAgentsByKnownTotal(query.data?.agents ?? []), + () => sortAgentsByDisplayTotal(query.data?.agents ?? []), [query.data?.agents], ); const pubkeys = React.useMemo( @@ -155,17 +152,13 @@ function AgentUsageCard({ series.coverage.invalidReportCount > 0; // Relative bars are decorative (aria-hidden, per plan) — scale each agent's - // known total (or i/o approximation) against the largest such value in the - // current window so the sorted-by-total list also reads as a bar chart. - const maxKnownTotal = React.useMemo( + // display total (exact or approximate) against the largest such value in the + // current window so the sorted-by-display-total list also reads as a bar chart. + const maxDisplayValue = React.useMemo( () => agents.reduce((max, agent) => { - const total = parseTokenCount(agent.usage.totalTokens.value); - if (total !== null) return total > max ? total : max; - // Fall back to the i/o approximation so bars are still visible when - // no agent reports a genuine total. - const approx = deriveApproxTotal(agent.usage); - return approx !== null && approx > max ? approx : max; + const dt = deriveDisplayTotal(agent.usage); + return dt.value !== null && dt.value > max ? dt.value : max; }, 0n), [agents], ); @@ -229,7 +222,7 @@ function AgentUsageCard({ days={days} key={agent.agentPubkey} label={resolveUserLabel({ profiles, pubkey: agent.agentPubkey })} - maxKnownTotal={maxKnownTotal} + maxDisplayValue={maxDisplayValue} onOpenAgentProfile={onOpenAgentProfile} profileAvatarUrl={ profiles?.[agent.agentPubkey]?.avatarUrl ?? null @@ -257,44 +250,28 @@ function AgentUsageRow({ agent, days, label, - maxKnownTotal, + maxDisplayValue, onOpenAgentProfile, profileAvatarUrl, }: { agent: AgentUsage; days: UsageWindowDays; label: string; - maxKnownTotal: bigint; + maxDisplayValue: bigint; onOpenAgentProfile: ( pubkey: string, options?: ProfilePanelOpenOptions, ) => void; profileAvatarUrl: string | null; }) { - const total = agent.usage.totalTokens; - const knownTotal = parseTokenCount(total.value); - const partial = isPartialField(total); - const unknown = isUnknownField(total); - - // When total is null (unknown), check whether any displayed I/O field is - // incomplete — per-field partial truth must be preserved at every seam (A2). - const ioPartial = - knownTotal === null && - (isPartialField(agent.usage.inputTokens) || - isPartialField(agent.usage.outputTokens)); - - // Approximate total for display when no genuine total is available. - const approxTotal = - knownTotal === null ? deriveApproxTotal(agent.usage) : null; - // Effective value for the relative bar width — prefer genuine total, then approx. - const barTotal = knownTotal ?? approxTotal; + const dt = deriveDisplayTotal(agent.usage); const trailing = - knownTotal !== null - ? formatTokenCountCompact(knownTotal) - : approxTotal !== null - ? `≈ ${formatTokenCountCompact(approxTotal)}` - : formatIndependentFields(agent); + dt.kind === "exact" + ? formatTokenCountCompact(dt.value) + : dt.kind === "approximate" + ? `≈ ${formatTokenCountCompact(dt.value)}` + : "No usage reported"; return ( ); } - -function formatIndependentFields(agent: AgentUsage): string { - const input = parseTokenCount(agent.usage.inputTokens.value); - const output = parseTokenCount(agent.usage.outputTokens.value); - if (input !== null || output !== null) { - const parts: string[] = []; - if (input !== null) parts.push(`in ${formatTokenCountCompact(input)}`); - if (output !== null) parts.push(`out ${formatTokenCountCompact(output)}`); - return parts.join(" · "); - } - return "No usage reported"; -} diff --git a/desktop/tests/e2e/agent-usage.spec.ts b/desktop/tests/e2e/agent-usage.spec.ts index ad8a16cb4..5f64d065a 100644 --- a/desktop/tests/e2e/agent-usage.spec.ts +++ b/desktop/tests/e2e/agent-usage.spec.ts @@ -816,9 +816,13 @@ test("focused view shows daily bars, coverage dates, and a partial explanation w await expect(coverage).toContainText("reported turn"); // The partial explanation must appear when usage is known-incomplete. - // (explainPartial = hasUnknownUsage || invalidReportCount > 0 — both true here.) + // The seed has hasUnknownUsage=true AND invalidReportCount=1, so both + // per-condition caveat sentences must appear independently. await expect( - page.getByTestId("agent-usage-focused-partial-explanation"), + page.getByTestId("agent-usage-focused-unknown-intervals-caveat"), + ).toBeVisible(); + await expect( + page.getByTestId("agent-usage-focused-invalid-reports-caveat"), ).toBeVisible(); });