feat(coding-agent): show cache hit rate in footer

This commit is contained in:
Mario Zechner
2026-06-05 10:01:11 +02:00
parent 3e73204242
commit db594d3a59
4 changed files with 32 additions and 1 deletions

View File

@@ -2,6 +2,10 @@
## [Unreleased]
### Added
- Added the latest prompt cache hit rate to the interactive footer.
### Fixed
- Fixed built-in tool expand hints to style closing parentheses consistently ([#5359](https://github.com/earendil-works/pi/issues/5359)).

View File

@@ -155,7 +155,7 @@ The interface from top to bottom:
- **Startup header** - Shows shortcuts (`/hotkeys` for all), loaded AGENTS.md files, prompt templates, skills, and extensions
- **Messages** - Your messages, assistant responses, tool calls and results, notifications, errors, and extension UI
- **Editor** - Where you type; border color indicates thinking level
- **Footer** - Working directory, session name, total token/cache usage, cost, context usage, current model
- **Footer** - Working directory, session name, total token/cache usage (`↑` input, `↓` output, `R` cache read, `W` cache write, `CH` latest cache hit rate), cost, context usage, current model
The editor can be temporarily replaced by other UI, like built-in `/settings` or custom UI from extensions (e.g., a Q&A tool that lets the user answer model questions in a structured format). [Extensions](#extensions) can also replace the editor, add widgets above/below it, a status line, custom footer, or overlays.

View File

@@ -88,6 +88,7 @@ export class FooterComponent implements Component {
let totalCacheRead = 0;
let totalCacheWrite = 0;
let totalCost = 0;
let latestCacheHitRate: number | undefined;
for (const entry of this.session.sessionManager.getEntries()) {
if (entry.type === "message" && entry.message.role === "assistant") {
@@ -96,6 +97,11 @@ export class FooterComponent implements Component {
totalCacheRead += entry.message.usage.cacheRead;
totalCacheWrite += entry.message.usage.cacheWrite;
totalCost += entry.message.usage.cost.total;
const latestPromptTokens =
entry.message.usage.input + entry.message.usage.cacheRead + entry.message.usage.cacheWrite;
latestCacheHitRate =
latestPromptTokens > 0 ? (entry.message.usage.cacheRead / latestPromptTokens) * 100 : undefined;
}
}
@@ -127,6 +133,9 @@ export class FooterComponent implements Component {
if (totalOutput) statsParts.push(`${formatTokens(totalOutput)}`);
if (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);
if (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);
if ((totalCacheRead > 0 || totalCacheWrite > 0) && latestCacheHitRate !== undefined) {
statsParts.push(`CH${latestCacheHitRate.toFixed(1)}%`);
}
// Show cost with "(sub)" indicator if using OAuth subscription
const usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;

View File

@@ -4,6 +4,7 @@ import type { AgentSession } from "../src/core/agent-session.ts";
import type { ReadonlyFooterDataProvider } from "../src/core/footer-data-provider.ts";
import { FooterComponent, formatCwdForFooter } from "../src/modes/interactive/components/footer.ts";
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
import { stripAnsi } from "../src/utils/ansi.ts";
type AssistantUsage = {
input: number;
@@ -123,4 +124,21 @@ describe("FooterComponent width handling", () => {
expect(visibleWidth(line)).toBeLessThanOrEqual(width);
}
});
it("shows the latest cache hit rate when cache usage is present", () => {
const session = createSession({
sessionName: "",
usage: {
input: 100,
output: 10,
cacheRead: 50,
cacheWrite: 50,
cost: { total: 0.001 },
},
});
const footer = new FooterComponent(session, createFooterData(1));
const statsLine = stripAnsi(footer.render(120)[1]);
expect(statsLine).toContain("CH25.0%");
});
});