Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0233d4c
feat(oauth): parse boosterWallet extra usage from /usages
liruifengv Jul 8, 2026
e051c10
feat(oauth): expose extraUsage on AuthManagedUsageResult
liruifengv Jul 8, 2026
7f458f6
feat(kimi-code): render Extra Usage section in /usage panel
liruifengv Jul 8, 2026
947725c
fix(kimi-code): address Task 3 review feedback for extra usage section
liruifengv Jul 8, 2026
9a4a20a
feat(kimi-code): render Extra Usage section in /status panel
liruifengv Jul 8, 2026
de3bf43
feat(kimi-code): wire extraUsage into /usage and /status commands
liruifengv Jul 8, 2026
a742241
chore(extra-usage): address final review findings for fuel pack feature
liruifengv Jul 8, 2026
f7c29fd
chore: temporarily log /usages raw response for debugging
liruifengv Jul 8, 2026
99103f1
fix(oauth): accept BOOSTER balance type and drop debug log
liruifengv Jul 9, 2026
facd2b9
fix(oauth): drop reset hint from Extra Usage and revert periodEnd par…
liruifengv Jul 9, 2026
ff8e409
fix(oauth): treat missing amountLeft as zero extra usage and drop deb…
liruifengv Jul 9, 2026
41642b6
revert: keep missing amountLeft defaulting to 0 (fully used)
liruifengv Jul 9, 2026
5df58b3
feat(extra-usage): show monthly cap usage bar and balance in /usage a…
liruifengv Jul 9, 2026
bfeb585
fix(extra-usage): show balance and unlimited marker when no monthly cap
liruifengv Jul 9, 2026
0b35f6d
fix(extra-usage): show monthly used with unlimited marker and balance
liruifengv Jul 9, 2026
9a83a2e
fix(extra-usage): label Used and use English Unlimited
liruifengv Jul 9, 2026
09e1576
feat(extra-usage): render balance, monthly used and monthly limit as …
liruifengv Jul 9, 2026
0a4adb5
fix(extra-usage): move Balance row to the bottom
liruifengv Jul 9, 2026
7927e50
fix(extra-usage): format currency values with two decimals for column…
liruifengv Jul 9, 2026
512383b
fix(extra-usage): right-align currency values so numbers line up
liruifengv Jul 9, 2026
ce260d6
fix(extra-usage): align currency symbol and decimal point in usage rows
liruifengv Jul 9, 2026
94867bc
Merge branch 'main' into feat/extra-usage-fuel-pack
liruifengv Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/expose-extrausage-toolkit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/kimi-code": patch
"@moonshot-ai/kimi-code-sdk": patch
---

Display Extra Usage (fuel pack) balance in `/usage` and `/status` commands.
2 changes: 1 addition & 1 deletion apps/kimi-code/src/tui/commands/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,5 +213,5 @@ async function loadManagedUsageReport(host: SlashCommandHost): Promise<ManagedUs
if (res.kind === 'error') {
return { error: res.message };
}
return { usage: { summary: res.summary, limits: res.limits } };
return { usage: { summary: res.summary, limits: res.limits, extraUsage: res.extraUsage } };
}
17 changes: 16 additions & 1 deletion apps/kimi-code/src/tui/components/messages/status-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ import {
safeUsageRatio,
} from '#/utils/usage/usage-format';

import { buildManagedUsageReportLines, type ManagedUsageReport } from './usage-panel';
import {
buildExtraUsageSection,
buildManagedUsageReportLines,
type ManagedUsageReport,
} from './usage-panel';

interface FieldRow {
readonly label: string;
Expand Down Expand Up @@ -145,5 +149,16 @@ export function buildStatusReportLines(options: StatusReportOptions): string[] {
lines.push(...managedSection);
}

const extraSection = buildExtraUsageSection(
options.managedUsage?.extraUsage,
accent,
value,
muted,
);
if (extraSection.length > 0) {
lines.push('');
lines.push(...extraSection);
}

return lines;
}
111 changes: 107 additions & 4 deletions apps/kimi-code/src/tui/components/messages/usage-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,19 @@ export interface ManagedUsageRow {
readonly resetHint?: string;
}

export interface BoosterWalletInfo {
readonly balanceCents: number;
readonly totalCents: number;
readonly monthlyChargeLimitEnabled: boolean;
readonly monthlyChargeLimitCents: number;
readonly monthlyUsedCents: number;
readonly currency: string;
}

export interface ManagedUsageReport {
readonly summary: ManagedUsageRow | null;
readonly limits: readonly ManagedUsageRow[];
readonly extraUsage?: BoosterWalletInfo | null;
}

export interface UsageReportOptions {
Expand Down Expand Up @@ -121,8 +131,7 @@ function buildManagedUsageSection(
r.limit > 0 ? Math.max(0, Math.min(r.used / r.limit, 1)) : 0;
const labelWidth = Math.max(10, ...rows.map((r) => r.label.length));
const pctWidth = Math.max(...rows.map((r) => `${Math.round(usedRatio(r) * 100)}% used`.length));
const severityColor = (sev: 'ok' | 'warn' | 'danger'): 'success' | 'warning' | 'error' =>
sev === 'danger' ? 'error' : sev === 'warn' ? 'warning' : 'success';

const out: string[] = [accent('Plan usage')];
for (const row of rows) {
const ratioUsed = usedRatio(row);
Expand All @@ -136,6 +145,91 @@ function buildManagedUsageSection(
return out;
}

function severityColor(sev: 'ok' | 'warn' | 'danger'): 'success' | 'warning' | 'error' {
return sev === 'danger' ? 'error' : sev === 'warn' ? 'warning' : 'success';
}

function currencySymbol(currency: string): string {
switch (currency.toUpperCase()) {
case 'CNY':
return '¥';
case 'USD':
return '$';
default:
return '';
}
}

interface CurrencyParts {
readonly symbol: string;
readonly number: string;
}

function formatCurrencyParts(cents: number, currency: string): CurrencyParts {
const symbol = currencySymbol(currency);
const main = cents / 100;
const formatted = main.toFixed(2);
return symbol.length > 0
? { symbol, number: formatted }
: { symbol: '', number: `${formatted} ${currency}` };
}

export function buildExtraUsageSection(
extraUsage: BoosterWalletInfo | undefined | null,
accent: Colorize,
value: Colorize,
muted: Colorize,
): string[] {
if (extraUsage === undefined || extraUsage === null) return [];

const hasMonthlyLimit =
extraUsage.monthlyChargeLimitEnabled && extraUsage.monthlyChargeLimitCents > 0;

const balance = formatCurrencyParts(extraUsage.balanceCents, extraUsage.currency);
const used = formatCurrencyParts(extraUsage.monthlyUsedCents, extraUsage.currency);
const rows: Array<{ label: string; symbol: string; number: string }> = [];
let barLine: string | null = null;

if (hasMonthlyLimit) {
const ratio = Math.max(
0,
Math.min(extraUsage.monthlyUsedCents / extraUsage.monthlyChargeLimitCents, 1),
);
const bar = renderProgressBar(ratio, 20);
barLine = ` ${currentTheme.fg(severityColor(ratioSeverity(ratio)), bar)}`;
const limit = formatCurrencyParts(extraUsage.monthlyChargeLimitCents, extraUsage.currency);
rows.push({ label: 'Used this month', ...used });
rows.push({ label: 'Monthly limit', ...limit });
rows.push({ label: 'Balance', ...balance });
} else {
rows.push({ label: 'Used this month', ...used });
rows.push({ label: 'Monthly limit', symbol: '', number: 'Unlimited' });
rows.push({ label: 'Balance', ...balance });
}

// `Used this month` is the longest label; size the column to the widest label
// so the currency symbol starts in the same column on every row.
const labelWidth = Math.max(...rows.map((r) => r.label.length));
// Right-align the numeric part of currency rows against each other so the
// decimal points line up (e.g. `¥ 50.00` / `¥200.00`). Text-only rows such as
// `Unlimited` carry no currency symbol, so they must not widen the numeric
// column — otherwise money values get padded with stray spaces.
const numberWidth = Math.max(
0,
...rows.filter((r) => r.symbol.length > 0).map((r) => visibleWidth(r.number)),
);
const row = (label: string, symbol: string, number: string): string => {
const cell = symbol.length > 0 ? symbol + number.padStart(numberWidth, ' ') : number;
return ` ${muted(label.padEnd(labelWidth, ' '))} ${value(cell)}`;
};

const lines: string[] = [accent('Extra Usage')];
if (barLine !== null) lines.push(barLine);
for (const r of rows) lines.push(row(r.label, r.symbol, r.number));

return lines;
}

export function buildManagedUsageReportLines(options: ManagedUsageReportLineOptions): string[] {
const accent = (text: string) => currentTheme.boldFg('primary', text);
const value = (text: string) => currentTheme.fg('text', text);
Expand All @@ -157,8 +251,6 @@ export function buildUsageReportLines(options: UsageReportOptions): string[] {
const value = (text: string) => currentTheme.fg('text', text);
const muted = (text: string) => currentTheme.fg('textDim', text);
const errorStyle = (text: string) => currentTheme.fg('error', text);
const severityColor = (sev: 'ok' | 'warn' | 'danger'): 'success' | 'warning' | 'error' =>
sev === 'danger' ? 'error' : sev === 'warn' ? 'warning' : 'success';

const lines: string[] = [
accent('Session usage'),
Expand Down Expand Up @@ -197,6 +289,17 @@ export function buildUsageReportLines(options: UsageReportOptions): string[] {
lines.push(...managedSection);
}

const extraSection = buildExtraUsageSection(
options.managedUsage?.extraUsage,
accent,
value,
muted,
);
if (extraSection.length > 0) {
lines.push('');
lines.push(...extraSection);
}

return lines;
}

Expand Down
38 changes: 38 additions & 0 deletions apps/kimi-code/test/tui/components/messages/status-panel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,44 @@ describe('status panel report lines', () => {
expect(output).not.toContain('Runtime');
});

it('formats extra usage section in status report', () => {
const lines = buildStatusReportLines({
version: '1.2.3',
model: 'k2',
workDir: '/tmp/project',
sessionId: 'ses-1',
sessionTitle: null,
thinkingEffort: 'off',
permissionMode: 'manual',
planMode: false,
contextUsage: 0,
contextTokens: 0,
maxContextTokens: 0,
availableModels: {},
managedUsage: {
summary: null,
limits: [],
extraUsage: {
balanceCents: 15000,
totalCents: 20000,
monthlyChargeLimitEnabled: true,
monthlyChargeLimitCents: 20000,
monthlyUsedCents: 5000,
currency: 'USD',
},
},
}).map(strip);

const output = lines.join('\n');
expect(output).toContain('Extra Usage');
expect(output).toContain('Balance');
expect(output).toContain('150.00');
expect(output).toContain('Used this month');
expect(output).toContain('50.00');
expect(output).toContain('Monthly limit');
expect(output).toContain('200.00');
});

it('falls back to app state and shows status load errors as warnings', () => {
const lines = buildStatusReportLines({
version: '1.2.3',
Expand Down
138 changes: 137 additions & 1 deletion apps/kimi-code/test/tui/components/messages/usage-panel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('UsagePanelComponent', () => {
output: 250,
},
},
} as never,
},
contextUsage: 0.25,
contextTokens: 2500,
maxContextTokens: 10000,
Expand All @@ -48,6 +48,142 @@ describe('UsagePanelComponent', () => {
expect(lines.join('\n')).toContain('resets tomorrow');
});

it('formats extra usage with a monthly limit', () => {
const lines = buildUsageReportLines({
sessionUsage: { byModel: {} },
contextUsage: 0,
contextTokens: 0,
maxContextTokens: 0,
managedUsage: {
summary: null,
limits: [],
extraUsage: {
balanceCents: 10000,
totalCents: 20000,
monthlyChargeLimitEnabled: true,
monthlyChargeLimitCents: 20000,
monthlyUsedCents: 5000,
currency: 'USD',
},
},
}).map(strip);

const output = lines.join('\n');
expect(lines).toContain('Extra Usage');
expect(output).toContain('Balance');
expect(output).toContain('100.00');
expect(output).toContain('Used this month');
expect(output).toContain('50.00');
expect(output).toContain('Monthly limit');
expect(output).toContain('200.00');
// bar row contains block glyphs but no percentage text
expect(output).toContain('░');
});

it('formats extra usage without a monthly limit and omits the progress bar', () => {
const lines = buildUsageReportLines({
sessionUsage: { byModel: {} },
contextUsage: 0,
contextTokens: 0,
maxContextTokens: 0,
managedUsage: {
summary: null,
limits: [],
extraUsage: {
balanceCents: 18208,
totalCents: 40000,
monthlyChargeLimitEnabled: false,
monthlyChargeLimitCents: 0,
monthlyUsedCents: 21792,
currency: 'CNY',
},
},
}).map(strip);

const output = lines.join('\n');
expect(lines).toContain('Extra Usage');
expect(output).toContain('Balance');
expect(output).toContain('¥182.08');
expect(output).toContain('Used this month');
expect(output).toContain('¥217.92');
expect(output).toContain('Monthly limit');
expect(output).toContain('Unlimited');
expect(output).not.toContain('░');
expect(output).not.toContain('█');
});

it('omits the extra usage section when extraUsage is omitted or null', () => {
for (const extraUsage of [undefined, null]) {
const lines = buildUsageReportLines({
sessionUsage: { byModel: {} },
contextUsage: 0,
contextTokens: 0,
maxContextTokens: 0,
managedUsage: { summary: null, limits: [], extraUsage },
}).map(strip);

expect(lines).not.toContain('Extra Usage');
}
});

it('formats extra usage with CNY currency', () => {
const lines = buildUsageReportLines({
sessionUsage: { byModel: {} },
contextUsage: 0,
contextTokens: 0,
maxContextTokens: 0,
managedUsage: {
summary: null,
limits: [],
extraUsage: {
balanceCents: 10000,
totalCents: 20000,
monthlyChargeLimitEnabled: true,
monthlyChargeLimitCents: 20000,
monthlyUsedCents: 5000,
currency: 'CNY',
},
},
}).map(strip);

const output = lines.join('\n');
expect(output).toContain('Balance');
expect(output).toContain('100.00');
expect(output).toContain('Used this month');
expect(output).toContain('50.00');
expect(output).toContain('Monthly limit');
expect(output).toContain('200.00');
});

it('aligns the currency symbol and decimal point across extra usage rows', () => {
const lines = buildUsageReportLines({
sessionUsage: { byModel: {} },
contextUsage: 0,
contextTokens: 0,
maxContextTokens: 0,
managedUsage: {
summary: null,
limits: [],
extraUsage: {
balanceCents: 15901,
totalCents: 300000,
monthlyChargeLimitEnabled: true,
monthlyChargeLimitCents: 300000,
monthlyUsedCents: 24099,
currency: 'CNY',
},
},
}).map(strip);

const extraRows = lines.filter((line) => line.includes('¥'));
expect(extraRows).toHaveLength(3);
// The currency symbol stays in one column...
expect(new Set(extraRows.map((line) => line.indexOf('¥'))).size).toBe(1);
// ...and the right-aligned numeric parts end in the same column, so the
// decimal points line up across rows.
expect(new Set(extraRows.map((line) => line.length)).size).toBe(1);
});

it('wraps preformatted usage lines in a bordered panel', () => {
const component = new UsagePanelComponent(() => ['Session usage'], 'primary');
const output = component.render(80).map(strip);
Expand Down
Loading
Loading