From 229fc745726c916a66046e503659890c7558073c Mon Sep 17 00:00:00 2001 From: happy wang Date: Fri, 29 May 2026 18:52:59 +0800 Subject: [PATCH 1/5] feat(tui): show prompt cache hit breakdown in /usage command --- .../tui/components/messages/usage-panel.ts | 11 +++ .../components/messages/usage-panel.test.ts | 98 ++++++++++++++++++- 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/apps/kimi-code/src/tui/components/messages/usage-panel.ts b/apps/kimi-code/src/tui/components/messages/usage-panel.ts index 0e4401a1..99de43df 100644 --- a/apps/kimi-code/src/tui/components/messages/usage-panel.ts +++ b/apps/kimi-code/src/tui/components/messages/usage-panel.ts @@ -90,6 +90,17 @@ function buildSessionUsageSection( formatTokenCount(output), )} total ${value(formatTokenCount(input + output))}`, ); + // Cache breakdown subline + const modelPrefix = ` ${model} `; + const cacheIndent = ' '.repeat(modelPrefix.length); + const cacheRatio = input > 0 ? usageNumber(row.inputCacheRead) / input : 0; + const bar = renderProgressBar(cacheRatio, 20); + const pct = `${Math.round(cacheRatio * 100)}%`; + lines.push( + `${cacheIndent}${muted('cache')} ${bar} ${value(pct)} ${muted('hit')} ` + + `(${value(formatTokenCount(usageNumber(row.inputCacheRead)))} ${muted('read')} ` + + `· ${value(formatTokenCount(usageNumber(row.inputOther)))} ${muted('other')})`, + ); } if (entries.length > 1) { lines.push( diff --git a/apps/kimi-code/test/tui/components/messages/usage-panel.test.ts b/apps/kimi-code/test/tui/components/messages/usage-panel.test.ts index 5e3883ca..42ca8fd0 100644 --- a/apps/kimi-code/test/tui/components/messages/usage-panel.test.ts +++ b/apps/kimi-code/test/tui/components/messages/usage-panel.test.ts @@ -9,6 +9,61 @@ function strip(text: string): string { } describe('UsagePanelComponent', () => { + it('shows cache hit ratio bar and read/other breakdown below each model line', () => { + const lines = buildUsageReportLines({ + colors: darkColors, + sessionUsage: { + byModel: { + kimi: { + inputOther: 1000, + inputCacheRead: 500, + inputCacheCreation: 500, + output: 250, + }, + }, + } as never, + contextUsage: 0.25, + contextTokens: 2500, + maxContextTokens: 10000, + }).map(strip); + + // Model line unchanged + expect(lines).toContain(' kimi input 2.0k output 250 total 2.3k'); + // Cache subline: indent=model prefix width, bar=5/20 filled, 25% hit, 500 read, 1.0k other + const cacheLine = lines.find((l) => l.includes('cache') && l.includes('hit')); + expect(cacheLine).toBeDefined(); + expect(cacheLine).toContain('25% hit'); + expect(cacheLine).toContain('500 read'); + expect(cacheLine).toContain('1.0k other'); + // Bar: 5 filled out of 20 + expect(cacheLine).toContain('█████░░░░░░░░░░░░░░░'); + }); + + it('shows zero cache hit ratio when no cache reads occurred', () => { + const lines = buildUsageReportLines({ + colors: darkColors, + sessionUsage: { + byModel: { + kimi: { + inputOther: 3000, + inputCacheRead: 0, + inputCacheCreation: 0, + output: 1000, + }, + }, + } as never, + contextUsage: 0, + contextTokens: 0, + maxContextTokens: 0, + }).map(strip); + + const cacheLine = lines.find((l) => l.includes('cache') && l.includes('hit')); + expect(cacheLine).toBeDefined(); + expect(cacheLine).toContain('0% hit'); + expect(cacheLine).toContain('0 read'); + expect(cacheLine).toContain('3.0k other'); + }); + it('formats session, context, and managed usage sections', () => { const lines = buildUsageReportLines({ colors: darkColors, @@ -37,7 +92,6 @@ describe('UsagePanelComponent', () => { }).map(strip); expect(lines).toContain('Session usage'); - expect(lines).toContain(' kimi input 2.0k output 250 total 2.3k'); expect(lines).toContain('Context window'); expect(lines.join('\n')).toContain('25.0%'); expect(lines).toContain('Plan usage'); @@ -45,6 +99,48 @@ describe('UsagePanelComponent', () => { expect(lines.join('\n')).toContain('resets tomorrow'); }); + it('shows separate cache lines for each model in multi-model sessions', () => { + const lines = buildUsageReportLines({ + colors: darkColors, + sessionUsage: { + byModel: { + 'kimi-k2.5': { + inputOther: 4000, + inputCacheRead: 6000, + inputCacheCreation: 200, + output: 500, + }, + 'deepseek-v4': { + inputOther: 2000, + inputCacheRead: 0, + inputCacheCreation: 0, + output: 1000, + }, + }, + } as never, + contextUsage: 0, + contextTokens: 0, + maxContextTokens: 0, + }).map(strip); + + // Both model lines present + expect(lines).toContain(' kimi-k2.5 input 10.2k output 500 total 10.7k'); + expect(lines).toContain(' deepseek-v4 input 2.0k output 1.0k total 3.0k'); + // Both have cache sublines + const cacheLines = lines.filter((l) => l.includes('cache') && l.includes('hit')); + expect(cacheLines).toHaveLength(2); + // kimi-k2.5: 6000/10200 ≈ 59% hit + expect(cacheLines[0]).toContain('59% hit'); + expect(cacheLines[0]).toContain('6.0k read'); + // deepseek-v4: 0% hit + expect(cacheLines[1]).toContain('0% hit'); + expect(cacheLines[1]).toContain('2.0k other'); + // Total line itself contains no cache info + const totalLine = lines.find((l) => l.startsWith(' total')); + expect(totalLine).toBeDefined(); + expect(totalLine!).not.toContain('cache'); + }); + it('wraps preformatted usage lines in a bordered panel', () => { const component = new UsagePanelComponent(['Session usage'], darkColors.primary); const output = component.render(80).map(strip); From 4685c1e93eb25ce1ef1f085ba10c55a361e95b31 Mon Sep 17 00:00:00 2001 From: happy wang Date: Fri, 29 May 2026 19:12:04 +0800 Subject: [PATCH 2/5] fix(tui): show one decimal in /usage cache hit ratio when not whole --- apps/kimi-code/src/tui/components/messages/usage-panel.ts | 2 +- .../test/tui/components/messages/usage-panel.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/kimi-code/src/tui/components/messages/usage-panel.ts b/apps/kimi-code/src/tui/components/messages/usage-panel.ts index 99de43df..113ef1e1 100644 --- a/apps/kimi-code/src/tui/components/messages/usage-panel.ts +++ b/apps/kimi-code/src/tui/components/messages/usage-panel.ts @@ -95,7 +95,7 @@ function buildSessionUsageSection( const cacheIndent = ' '.repeat(modelPrefix.length); const cacheRatio = input > 0 ? usageNumber(row.inputCacheRead) / input : 0; const bar = renderProgressBar(cacheRatio, 20); - const pct = `${Math.round(cacheRatio * 100)}%`; + const pct = `${(cacheRatio * 100).toFixed(1).replace(/\.0$/, '')}%`; lines.push( `${cacheIndent}${muted('cache')} ${bar} ${value(pct)} ${muted('hit')} ` + `(${value(formatTokenCount(usageNumber(row.inputCacheRead)))} ${muted('read')} ` + diff --git a/apps/kimi-code/test/tui/components/messages/usage-panel.test.ts b/apps/kimi-code/test/tui/components/messages/usage-panel.test.ts index 42ca8fd0..aeb13adc 100644 --- a/apps/kimi-code/test/tui/components/messages/usage-panel.test.ts +++ b/apps/kimi-code/test/tui/components/messages/usage-panel.test.ts @@ -129,8 +129,8 @@ describe('UsagePanelComponent', () => { // Both have cache sublines const cacheLines = lines.filter((l) => l.includes('cache') && l.includes('hit')); expect(cacheLines).toHaveLength(2); - // kimi-k2.5: 6000/10200 ≈ 59% hit - expect(cacheLines[0]).toContain('59% hit'); + // kimi-k2.5: 6000/10200 ≈ 58.8% hit + expect(cacheLines[0]).toContain('58.8% hit'); expect(cacheLines[0]).toContain('6.0k read'); // deepseek-v4: 0% hit expect(cacheLines[1]).toContain('0% hit'); From bbb46228780c9ffd3c635eee057a20bc9c56505b Mon Sep 17 00:00:00 2001 From: happy wang Date: Sat, 30 May 2026 00:20:51 +0800 Subject: [PATCH 3/5] fix(tui): align /usage model names to max width for clean layout --- .../src/tui/components/messages/usage-panel.ts | 14 ++++++++++---- .../components/messages/usage-panel.test.ts | 18 +++++++++++++++--- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/apps/kimi-code/src/tui/components/messages/usage-panel.ts b/apps/kimi-code/src/tui/components/messages/usage-panel.ts index 113ef1e1..b02cb576 100644 --- a/apps/kimi-code/src/tui/components/messages/usage-panel.ts +++ b/apps/kimi-code/src/tui/components/messages/usage-panel.ts @@ -80,19 +80,24 @@ function buildSessionUsageSection( const lines: string[] = []; let totalInput = 0; let totalOutput = 0; + // Compute max model name width for alignment (include "total" for multi-model) + const maxModelWidth = Math.max( + ...entries.map(([model]) => model.length), + entries.length > 1 ? 'total'.length : 0, + ); for (const [model, row] of entries) { const input = usageInputTotal(row); const output = usageNumber(row.output); totalInput += input; totalOutput += output; + const paddedModel = model.padEnd(maxModelWidth); lines.push( - ` ${muted(model)} input ${value(formatTokenCount(input))} output ${value( + ` ${muted(paddedModel)} input ${value(formatTokenCount(input))} output ${value( formatTokenCount(output), )} total ${value(formatTokenCount(input + output))}`, ); // Cache breakdown subline - const modelPrefix = ` ${model} `; - const cacheIndent = ' '.repeat(modelPrefix.length); + const cacheIndent = ' '.repeat(maxModelWidth + 4); // " model " → 2 + maxModelWidth + 2 const cacheRatio = input > 0 ? usageNumber(row.inputCacheRead) / input : 0; const bar = renderProgressBar(cacheRatio, 20); const pct = `${(cacheRatio * 100).toFixed(1).replace(/\.0$/, '')}%`; @@ -103,8 +108,9 @@ function buildSessionUsageSection( ); } if (entries.length > 1) { + const paddedTotal = 'total'.padEnd(maxModelWidth); lines.push( - ` ${muted('total')} input ${value(formatTokenCount(totalInput))} output ${value( + ` ${muted(paddedTotal)} input ${value(formatTokenCount(totalInput))} output ${value( formatTokenCount(totalOutput), )} total ${value(formatTokenCount(totalInput + totalOutput))}`, ); diff --git a/apps/kimi-code/test/tui/components/messages/usage-panel.test.ts b/apps/kimi-code/test/tui/components/messages/usage-panel.test.ts index aeb13adc..1f9c23b7 100644 --- a/apps/kimi-code/test/tui/components/messages/usage-panel.test.ts +++ b/apps/kimi-code/test/tui/components/messages/usage-panel.test.ts @@ -123,9 +123,21 @@ describe('UsagePanelComponent', () => { maxContextTokens: 0, }).map(strip); - // Both model lines present - expect(lines).toContain(' kimi-k2.5 input 10.2k output 500 total 10.7k'); - expect(lines).toContain(' deepseek-v4 input 2.0k output 1.0k total 3.0k'); + // Both model lines present (model padded to max width for alignment) + const modelLines = lines.filter( + (l) => l.startsWith(' ') && (l.includes('kimi-k2.5') || l.includes('deepseek-v4')) + && l.includes('input') && l.includes('output') && l.includes('total') + && !l.includes('cache'), + ); + expect(modelLines).toHaveLength(2); + expect(modelLines[0]).toContain('kimi-k2.5'); + expect(modelLines[0]).toContain('10.2k'); + expect(modelLines[0]).toContain('500'); + expect(modelLines[0]).toContain('10.7k'); + expect(modelLines[1]).toContain('deepseek-v4'); + expect(modelLines[1]).toContain('2.0k'); + expect(modelLines[1]).toContain('1.0k'); + expect(modelLines[1]).toContain('3.0k'); // Both have cache sublines const cacheLines = lines.filter((l) => l.includes('cache') && l.includes('hit')); expect(cacheLines).toHaveLength(2); From 311f4deb66dd0184ceef083bbb18c2c10b640e55 Mon Sep 17 00:00:00 2001 From: happy wang Date: Sat, 30 May 2026 00:24:57 +0800 Subject: [PATCH 4/5] chore: add changeset for /usage cache breakdown --- .changeset/usage-cache-breakdown.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/usage-cache-breakdown.md diff --git a/.changeset/usage-cache-breakdown.md b/.changeset/usage-cache-breakdown.md new file mode 100644 index 00000000..ae25beff --- /dev/null +++ b/.changeset/usage-cache-breakdown.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": minor +--- + +Show prompt cache hit rate and read/other breakdown in /usage command. From aaada0867258a2465513abb83a37faccd0da9d23 Mon Sep 17 00:00:00 2001 From: happy wang Date: Sat, 30 May 2026 00:32:37 +0800 Subject: [PATCH 5/5] fix(tui): guard empty entries and extract progress bar width constant --- .../src/tui/components/messages/usage-panel.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/kimi-code/src/tui/components/messages/usage-panel.ts b/apps/kimi-code/src/tui/components/messages/usage-panel.ts index b02cb576..1a99f6f7 100644 --- a/apps/kimi-code/src/tui/components/messages/usage-panel.ts +++ b/apps/kimi-code/src/tui/components/messages/usage-panel.ts @@ -20,6 +20,7 @@ import type { ColorPalette } from '#/tui/theme/colors'; const LEFT_MARGIN = 2; const SIDE_PADDING = 1; const MIN_INTERIOR_WIDTH = 20; +const PROGRESS_BAR_WIDTH = 20; type Colorize = (text: string) => string; @@ -81,10 +82,13 @@ function buildSessionUsageSection( let totalInput = 0; let totalOutput = 0; // Compute max model name width for alignment (include "total" for multi-model) - const maxModelWidth = Math.max( - ...entries.map(([model]) => model.length), - entries.length > 1 ? 'total'.length : 0, - ); + const maxModelWidth = + entries.length === 0 + ? 0 + : Math.max( + ...entries.map(([model]) => model.length), + entries.length > 1 ? 'total'.length : 0, + ); for (const [model, row] of entries) { const input = usageInputTotal(row); const output = usageNumber(row.output); @@ -99,7 +103,7 @@ function buildSessionUsageSection( // Cache breakdown subline const cacheIndent = ' '.repeat(maxModelWidth + 4); // " model " → 2 + maxModelWidth + 2 const cacheRatio = input > 0 ? usageNumber(row.inputCacheRead) / input : 0; - const bar = renderProgressBar(cacheRatio, 20); + const bar = renderProgressBar(cacheRatio, PROGRESS_BAR_WIDTH); const pct = `${(cacheRatio * 100).toFixed(1).replace(/\.0$/, '')}%`; lines.push( `${cacheIndent}${muted('cache')} ${bar} ${value(pct)} ${muted('hit')} ` + @@ -144,7 +148,7 @@ function buildManagedUsageSection( const out: string[] = [accent('Plan usage')]; for (const row of rows) { const ratioUsed = usedRatio(row); - const bar = renderProgressBar(ratioUsed, 20); + const bar = renderProgressBar(ratioUsed, PROGRESS_BAR_WIDTH); const pct = `${Math.round(ratioUsed * 100)}% used`; const barColoured = chalk.hex(severityHex(ratioSeverity(ratioUsed)))(bar); const label = row.label.padEnd(labelWidth, ' '); @@ -196,7 +200,7 @@ export function buildUsageReportLines(options: UsageReportOptions): string[] { if (options.maxContextTokens > 0) { const ratio = safeUsageRatio(options.contextUsage); - const bar = renderProgressBar(ratio, 20); + const bar = renderProgressBar(ratio, PROGRESS_BAR_WIDTH); const pct = `${(ratio * 100).toFixed(1)}%`; const barColoured = chalk.hex(severityHex(ratioSeverity(ratio)))(bar); lines.push('');