diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 119c29b8..6cd0b184 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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)). diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 60e8da1a..e86d8c62 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -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. diff --git a/packages/coding-agent/src/modes/interactive/components/footer.ts b/packages/coding-agent/src/modes/interactive/components/footer.ts index 47ff58ae..f108ae5e 100644 --- a/packages/coding-agent/src/modes/interactive/components/footer.ts +++ b/packages/coding-agent/src/modes/interactive/components/footer.ts @@ -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; diff --git a/packages/coding-agent/test/footer-width.test.ts b/packages/coding-agent/test/footer-width.test.ts index d5586d7d..51df1eda 100644 --- a/packages/coding-agent/test/footer-width.test.ts +++ b/packages/coding-agent/test/footer-width.test.ts @@ -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%"); + }); });