From b01a381a4696b1bcbfb0f0d8a160eb792011d94c Mon Sep 17 00:00:00 2001 From: Duncan Date: Wed, 12 Aug 2026 18:09:16 -0400 Subject: [PATCH] feat(desktop): display cache-read/write/fresh-input breakdown in agent-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 Signed-off-by: Will Pfleger --- .../agent-usage/lib/agentUsage.test.mjs | 81 +++++++++ .../features/agent-usage/lib/agentUsage.ts | 51 ++++++ .../agent-usage/ui/AgentUsageFocusedView.tsx | 115 ++++++++++--- .../tests/e2e/agent-usage-screenshots.spec.ts | 18 +- desktop/tests/e2e/agent-usage.spec.ts | 162 +++++++++++++++++- 5 files changed, 393 insertions(+), 34 deletions(-) diff --git a/desktop/src/features/agent-usage/lib/agentUsage.test.mjs b/desktop/src/features/agent-usage/lib/agentUsage.test.mjs index 0ec696680..5ede4c869 100644 --- a/desktop/src/features/agent-usage/lib/agentUsage.test.mjs +++ b/desktop/src/features/agent-usage/lib/agentUsage.test.mjs @@ -13,8 +13,10 @@ import { formatCoverageDate, formatEstimatedCostUsd, formatLocalDate, + formatModelCacheBreakdown, formatTokenCountCompact, formatTokenCountExact, + hasKnownCacheData, isPartialField, isUnknownField, MAX_RANGE_DAYS, @@ -443,6 +445,85 @@ test("isPartialField and isUnknownField classify usage fields correctly", () => assert.equal(isUnknownField(usageField({ value: "0" })), false); }); +// ── formatModelCacheBreakdown / hasKnownCacheData ──────────────────────────── + +test("hasKnownCacheData is true iff any cache subset carries a known value", () => { + assert.equal(hasKnownCacheData(reportedUsage()), false, "all absent → false"); + for (const field of [ + "cacheReadTokens", + "cacheWriteTokens", + "freshInputTokens", + ]) { + assert.equal( + hasKnownCacheData(reportedUsage({ [field]: usageField({ value: "0" }) })), + true, + `a known ${field} (even zero) → true`, + ); + } + // A field that is incomplete but has no value is still unknown, not known. + assert.equal( + hasKnownCacheData( + reportedUsage({ + cacheReadTokens: usageField({ value: null, incomplete: true }), + }), + ), + false, + "incomplete with null value is unknown, not known", + ); +}); + +test("formatModelCacheBreakdown omits absent subsets, never renders them as zero, and marks a known lower bound Partial", () => { + const build = (overrides) => + modelUsage("m", null, { usage: reportedUsage(overrides) }); + + // No cache data at all → null, so the caller omits the line entirely. + assert.equal(formatModelCacheBreakdown(build({})), null); + + // Each known subset appears compact-formatted; absent ones are omitted + // rather than shown as "0". + assert.equal( + formatModelCacheBreakdown( + build({ + cacheReadTokens: usageField({ value: "1500" }), + cacheWriteTokens: usageField({ value: "300" }), + freshInputTokens: usageField({ value: "1200000" }), + }), + ), + "Cache read 1.5K · Cache write 300 · Fresh 1.2M", + ); + + // Only cache-read known — the other two are unknown and omitted, not zero. + assert.equal( + formatModelCacheBreakdown( + build({ cacheReadTokens: usageField({ value: "800" }) }), + ), + "Cache read 800", + ); + + // An incomplete known field appends a single trailing Partial marker. + assert.equal( + formatModelCacheBreakdown( + build({ + cacheReadTokens: usageField({ value: "800", incomplete: true }), + cacheWriteTokens: usageField({ value: "200" }), + }), + ), + "Cache read 800 · Cache write 200 · Partial", + ); + + // An incomplete field with NO value carries no known lower bound: it is + // omitted and does not trigger Partial on its own. + assert.equal( + formatModelCacheBreakdown( + build({ + cacheReadTokens: usageField({ value: "800" }), + cacheWriteTokens: usageField({ value: null, incomplete: true }), + }), + ), + "Cache read 800", + ); +}); + // ── sumKnownBucketTotals ────────────────────────────────────────────────────── function bucket(overrides = {}) { diff --git a/desktop/src/features/agent-usage/lib/agentUsage.ts b/desktop/src/features/agent-usage/lib/agentUsage.ts index 41fe276d6..99ddd71f4 100644 --- a/desktop/src/features/agent-usage/lib/agentUsage.ts +++ b/desktop/src/features/agent-usage/lib/agentUsage.ts @@ -534,6 +534,57 @@ export function isUnknownField(field: UsageField | CostField): boolean { return field.value === null; } +/** + * Compact per-model input breakdown for the focused view: the cache-read, + * cache-write, and fresh-input subsets of input, each shown only when known + * (`value !== null`) and never as zero. Returns `null` when no subset is + * known at all, so the caller omits the line entirely rather than printing + * three unknowns. A trailing ` · Partial` marks that at least one shown field + * is a known-but-incomplete lower bound — matching the row's Partial badge and + * {@link deriveUsageIngressTrailing}'s text convention. Absent (`null`) fields + * are simply omitted, never marked Partial (they carry no known lower bound). + */ +export function formatModelCacheBreakdown( + model: AgentUsageModel, +): string | null { + const { cacheReadTokens, cacheWriteTokens, freshInputTokens } = model.usage; + const parts: string[] = []; + const push = (label: string, field: UsageField) => { + const parsed = parseTokenCount(field.value); + if (parsed !== null) { + parts.push(`${label} ${formatTokenCountCompact(parsed)}`); + } + }; + push("Cache read", cacheReadTokens); + push("Cache write", cacheWriteTokens); + push("Fresh", freshInputTokens); + if (parts.length === 0) return null; + const partial = + isPartialField(cacheReadTokens) || + isPartialField(cacheWriteTokens) || + isPartialField(freshInputTokens); + return partial ? `${parts.join(" · ")} · Partial` : parts.join(" · "); +} + +/** + * True when a usage scope has any known cache-read, cache-write, or + * fresh-input value — the gate for showing the focused view's input-breakdown + * subsection. When every cache subset is unknown (old-harness data that never + * reported cache tokens), the subsection is omitted rather than rendering a + * row of "—", which keeps absence honest without visual noise. + */ +export function hasKnownCacheData(usage: { + cacheReadTokens: UsageField; + cacheWriteTokens: UsageField; + freshInputTokens: UsageField; +}): boolean { + return ( + !isUnknownField(usage.cacheReadTokens) || + !isUnknownField(usage.cacheWriteTokens) || + !isUnknownField(usage.freshInputTokens) + ); +} + /** * Truthful trailing summary for the profile Info-tab Usage ingress row * (plan:328): the viewer's own agent's 7-day known total, `Partial` when diff --git a/desktop/src/features/agent-usage/ui/AgentUsageFocusedView.tsx b/desktop/src/features/agent-usage/ui/AgentUsageFocusedView.tsx index 4d7f677ba..7414c9303 100644 --- a/desktop/src/features/agent-usage/ui/AgentUsageFocusedView.tsx +++ b/desktop/src/features/agent-usage/ui/AgentUsageFocusedView.tsx @@ -18,8 +18,10 @@ import { describeRange, formatCoverageDate, formatEstimatedCostUsd, + formatModelCacheBreakdown, formatTokenCountCompact, formatTokenCountExact, + hasKnownCacheData, isPartialField, isUnknownField, parseTokenCount, @@ -218,6 +220,8 @@ function AgentUsageFocusedTotals({ // Display total for the Total tokens stat. const displayTotal = deriveDisplayTotal(agent.usage); + const { cacheReadTokens, cacheWriteTokens, freshInputTokens } = agent.usage; + const showCacheBreakdown = hasKnownCacheData(agent.usage); return ( @@ -236,6 +240,34 @@ function AgentUsageFocusedTotals({ /> + {showCacheBreakdown ? ( +
+

+ Input breakdown +

+
+ + + +
+
+ ) : null} + {agent.buckets.length > 0 ? (

By model

{models.map((model) => ( -
- - {model.model ?? "Unknown model"} - {model.harness !== null ? ( - - {model.harness} - - ) : null} - - - {isUnknownField(model.usage.totalTokens) - ? formatModelIndependentFields(model) - : formatTokenCountExact( - parseTokenCount(model.usage.totalTokens.value) ?? 0n, - )} - {isPartialField(model.usage.totalTokens) || - isModelIoPartial(model) ? ( - - Partial - - ) : null} - -
+ model={model} + /> ))}
) : null} @@ -318,9 +325,11 @@ function AgentUsageFocusedTotals({ function TokenStat({ field, label, + testId, }: { field: { value: string | null; incomplete: boolean }; label: string; + testId?: string; }) { const parsed = parseTokenCount(field.value); return ( @@ -328,6 +337,7 @@ function TokenStat({ display={parsed !== null ? formatTokenCountExact(parsed) : null} isPartial={isPartialField(field)} label={label} + testId={testId} /> ); } @@ -400,6 +410,55 @@ function formatCoverageRange(coverage: AgentUsageSeries["coverage"]): string { return `${formatCoverageDate(firstReportedAt)} – ${formatCoverageDate(lastReportedAt)}`; } +/** + * One "By model" row: the model/harness label with its display total and + * Partial badge, plus a muted cache/fresh-input breakdown line beneath it + * when any cache subset is known. The breakdown is computed once and omitted + * entirely when no subset was reported, so old-harness models read no + * differently than before. + */ +function FocusedModelRow({ model }: { model: AgentUsageModel }) { + const cacheBreakdown = formatModelCacheBreakdown(model); + return ( +
+
+ + {model.model ?? "Unknown model"} + {model.harness !== null ? ( + + {model.harness} + + ) : null} + + + {isUnknownField(model.usage.totalTokens) + ? formatModelIndependentFields(model) + : formatTokenCountExact( + parseTokenCount(model.usage.totalTokens.value) ?? 0n, + )} + {isPartialField(model.usage.totalTokens) || + isModelIoPartial(model) ? ( + + Partial + + ) : null} + +
+ {cacheBreakdown !== null ? ( +

+ {cacheBreakdown} +

+ ) : null} +
+ ); +} + /** * Render known model I/O fields when the model total is unknown — never * collapses to "No usage reported" when input or output is actually known diff --git a/desktop/tests/e2e/agent-usage-screenshots.spec.ts b/desktop/tests/e2e/agent-usage-screenshots.spec.ts index c1145c644..f797e71cd 100644 --- a/desktop/tests/e2e/agent-usage-screenshots.spec.ts +++ b/desktop/tests/e2e/agent-usage-screenshots.spec.ts @@ -24,6 +24,9 @@ function reportedUsage( outputTokens: string | null; totalTokens: string | null; estimatedCostUsd: number | null; + cacheReadTokens: string | null; + cacheWriteTokens: string | null; + freshInputTokens: string | null; }> = {}, ) { return { @@ -31,9 +34,9 @@ function reportedUsage( inputTokens: usageField(overrides.inputTokens ?? null), outputTokens: usageField(overrides.outputTokens ?? null), totalTokens: usageField(overrides.totalTokens ?? null), - cacheReadTokens: usageField(null), - cacheWriteTokens: usageField(null), - freshInputTokens: usageField(null), + cacheReadTokens: usageField(overrides.cacheReadTokens ?? null), + cacheWriteTokens: usageField(overrides.cacheWriteTokens ?? null), + freshInputTokens: usageField(overrides.freshInputTokens ?? null), }; } @@ -296,6 +299,9 @@ test.describe("agent usage screenshots", () => { inputTokens: "2100", outputTokens: "700", estimatedCostUsd: 0.35, + cacheReadTokens: "1400", + cacheWriteTokens: "300", + freshInputTokens: "400", }), }, { @@ -308,6 +314,9 @@ test.describe("agent usage screenshots", () => { inputTokens: "600", outputTokens: "200", estimatedCostUsd: 0.04, + cacheReadTokens: "350", + cacheWriteTokens: "50", + freshInputTokens: "200", }), }, ], @@ -316,6 +325,9 @@ test.describe("agent usage screenshots", () => { inputTokens: "2700", outputTokens: "900", totalTokens: "3600", + cacheReadTokens: "1750", + cacheWriteTokens: "350", + freshInputTokens: "600", }), }), ], diff --git a/desktop/tests/e2e/agent-usage.spec.ts b/desktop/tests/e2e/agent-usage.spec.ts index 87b621bf3..8a193c777 100644 --- a/desktop/tests/e2e/agent-usage.spec.ts +++ b/desktop/tests/e2e/agent-usage.spec.ts @@ -55,6 +55,9 @@ function reportedUsage( outputTokens: string | null; totalTokens: string | null; estimatedCostUsd: number | null; + cacheReadTokens: string | null; + cacheWriteTokens: string | null; + freshInputTokens: string | null; }> = {}, ) { return { @@ -62,9 +65,9 @@ function reportedUsage( inputTokens: usageField(overrides.inputTokens ?? null), outputTokens: usageField(overrides.outputTokens ?? null), totalTokens: usageField(overrides.totalTokens ?? null), - cacheReadTokens: usageField(null), - cacheWriteTokens: usageField(null), - freshInputTokens: usageField(null), + cacheReadTokens: usageField(overrides.cacheReadTokens ?? null), + cacheWriteTokens: usageField(overrides.cacheWriteTokens ?? null), + freshInputTokens: usageField(overrides.freshInputTokens ?? null), }; } @@ -994,6 +997,159 @@ test("focused view renders the unknown-intervals and invalid-reports caveats und }); }); +test("the focused view shows the cache/fresh-input breakdown with exact totals and a per-model line when cache data is present", async ({ + page, +}) => { + await installMockBridge(page); + await openAgentsView(page); + + const agentPubkey = await addGenericAgent(page, "general", "Cache Bot"); + await seedSeries( + page, + mockUsageSeries({ + agents: [ + mockAgentUsage(agentPubkey, { + models: [ + { + harness: "claude-code", + hasUnknownUsage: false, + model: "claude-opus", + reportCount: 1, + usage: reportedUsage({ + totalTokens: "12000", + inputTokens: "10000", + outputTokens: "2000", + cacheReadTokens: "6000", + cacheWriteTokens: "1500", + freshInputTokens: "2500", + }), + }, + ], + usage: reportedUsage({ + inputTokens: "10000", + outputTokens: "2000", + totalTokens: "12000", + cacheReadTokens: "6000", + cacheWriteTokens: "1500", + freshInputTokens: "2500", + }), + }), + ], + }), + ); + + await page.getByTestId(`agent-usage-row-${agentPubkey}`).click(); + await expect(page.getByTestId("agent-usage-focused-view")).toBeVisible(); + + // Input-breakdown subsection: three exact stats, none rendered as zero. + const cache = page.getByTestId("agent-usage-focused-cache"); + await expect(cache).toBeVisible(); + await expect(cache).toContainText("Cache read"); + await expect( + page.getByTestId("agent-usage-focused-cache-read-value"), + ).toHaveText("6,000"); + await expect( + page.getByTestId("agent-usage-focused-cache-write-value"), + ).toHaveText("1,500"); + await expect( + page.getByTestId("agent-usage-focused-fresh-input-value"), + ).toHaveText("2,500"); + // No Partial badge when every shown cache field is a complete value. + await expect(cache.getByText("Partial", { exact: true })).toHaveCount(0); + + // Per-model breakdown line carries the same subsets, compact-formatted. + const modelCache = page.getByTestId("agent-usage-model-cache-breakdown"); + await expect(modelCache).toContainText("Cache read 6K"); + await expect(modelCache).toContainText("Cache write 1.5K"); + await expect(modelCache).toContainText("Fresh 2.5K"); +}); + +test("the focused view marks a known-but-incomplete cache field Partial, omits absent subsets, and hides the breakdown entirely when no cache data exists", async ({ + page, +}) => { + await installMockBridge(page); + await openAgentsView(page); + + const agentPubkey = await addGenericAgent( + page, + "general", + "Partial Cache Bot", + ); + + await test.step("mixed cache completeness → Partial on the lower bound, absent fresh-input shows em-dash", async () => { + await seedSeries( + page, + mockUsageSeries({ + agents: [ + mockAgentUsage(agentPubkey, { + usage: { + estimatedCostUsd: costField(null), + inputTokens: usageField("10000"), + outputTokens: usageField("2000"), + totalTokens: usageField("12000"), + // Known but incomplete (Will's mixed-window shape) → Partial. + cacheReadTokens: usageField("6000", true), + // Absent → must render "—", never 0. + cacheWriteTokens: usageField(null), + // Fail-closed: fresh input cannot be derived → unknown → "—". + freshInputTokens: usageField(null, true), + }, + }), + ], + }), + ); + + await page.getByTestId(`agent-usage-row-${agentPubkey}`).click(); + const cache = page.getByTestId("agent-usage-focused-cache"); + await expect(cache).toBeVisible(); + // Cache read: known lower bound → value plus Partial badge. + await expect( + page.getByTestId("agent-usage-focused-cache-read-value"), + ).toHaveText("6,000"); + await expect(cache.getByText("Partial", { exact: true })).toBeVisible(); + // Cache write + fresh input are unknown → em-dash, never zero. + await expect( + page.getByTestId("agent-usage-focused-cache-write-value"), + ).toHaveText("—"); + await expect( + page.getByTestId("agent-usage-focused-fresh-input-value"), + ).toHaveText("—"); + }); + + await test.step("no cache data at all → the breakdown subsection is absent", async () => { + await seedSeries( + page, + mockUsageSeries({ + agents: [ + mockAgentUsage(agentPubkey, { + // Old-harness shape: i/o known, every cache subset absent. + usage: reportedUsage({ + inputTokens: "10000", + outputTokens: "2000", + totalTokens: "12000", + }), + }), + ], + }), + ); + await page.evaluate(() => + ( + window as Window & { + __BUZZ_E2E_QUERY_CLIENT__?: { + invalidateQueries: () => Promise; + }; + } + ).__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries(), + ); + await page.getByTestId(`agent-usage-row-${agentPubkey}`).click(); + await expect(page.getByTestId("agent-usage-focused-totals")).toBeVisible(); + await expect(page.getByTestId("agent-usage-focused-cache")).toHaveCount(0); + await expect( + page.getByTestId("agent-usage-model-cache-breakdown"), + ).toHaveCount(0); + }); +}); + test("overview and focused view distinguish an invalid-only window from ordinary empty windows", async ({ page, }) => {