Skip to content

Commit 8e677aa

Browse files
feat(tui): align footer quota columns and include reset countdown
1 parent f305deb commit 8e677aa

2 files changed

Lines changed: 62 additions & 40 deletions

File tree

apps/kimi-code/src/tui/components/chrome/footer.ts

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Layout:
55
* Line 1: [yolo] [plan] <model> <cwd> <git-badge> <shortcut hints>
66
* Line 2: context: XX.X% (tokens/max)
7-
* Line 3+: aligned quota rows: "label (used%/100%)" under context parens
7+
* Line 3+: right-aligned quota rows: "label: XX% (reset in ...)"
88
*/
99

1010
import type { Component } from '@earendil-works/pi-tui';
@@ -220,36 +220,52 @@ function hslToHex(h: number, s: number, l: number): string {
220220
return `#${f(0)}${f(8)}${f(4)}`;
221221
}
222222

223+
function formatResetHint(hint: string | undefined): string {
224+
if (hint === undefined) return '';
225+
if (hint === 'reset') return '(reset)';
226+
if (hint.startsWith('resets in ')) {
227+
const duration = hint.slice('resets in '.length).replace(/ /g, ', ');
228+
return `(${duration})`;
229+
}
230+
return `(${hint})`;
231+
}
232+
223233
function formatQuotaLines(
224234
quotas: readonly QuotaInfo[] | undefined,
225-
contextText: string,
226-
contextWidth: number,
227235
width: number,
228236
colors: ColorPalette,
229237
): string[] {
230238
if (quotas === undefined || quotas.length === 0) return [];
231239

232-
const parenIndex = contextText.indexOf('(');
233-
const parenCol =
234-
parenIndex >= 0 ? Math.max(0, width - contextWidth + parenIndex) : width;
240+
const rows = quotas
241+
.filter((quota) => quota.limit > 0)
242+
.map((quota) => {
243+
const usedRatio = Math.max(0, Math.min(quota.used / quota.limit, 1));
244+
return {
245+
label: `${quota.label.toLowerCase()}:`,
246+
percent: `${Math.round(usedRatio * 100)}%`,
247+
reset: formatResetHint(quota.resetHint),
248+
ratio: usedRatio,
249+
};
250+
});
251+
if (rows.length === 0) return [];
252+
253+
const labelColWidth = Math.max(...rows.map((r) => visibleWidth(r.label)));
254+
const percentColWidth = Math.max(...rows.map((r) => visibleWidth(r.percent)));
255+
const resetColWidth = Math.max(...rows.map((r) => visibleWidth(r.reset)));
256+
const gap = 3;
257+
const blockWidth = labelColWidth + gap + percentColWidth + gap + resetColWidth;
235258

236259
const lines: string[] = [];
237-
for (const quota of quotas) {
238-
if (quota.limit <= 0) continue;
239-
const usedRatio = Math.max(0, Math.min(quota.used / quota.limit, 1));
240-
const usedPct = Math.round(usedRatio * 100);
241-
// Gradient from dark green (0%) to red (100%).
242-
const numberColor = chalk.hex(hslToHex(Math.round((1 - usedRatio) * 120), 80, 40));
243-
const prefix = `${quota.label.toLowerCase()} `;
244-
const prefixWidth = visibleWidth(prefix);
245-
const pad = Math.max(0, parenCol - prefixWidth);
246-
const line =
247-
' '.repeat(pad) +
248-
chalk.hex(colors.text)(prefix) +
249-
chalk.hex(colors.text)('(') +
250-
numberColor(String(usedPct)) +
251-
chalk.hex(colors.text)('/100%)');
252-
lines.push(truncateToWidth(line, width));
260+
for (const row of rows) {
261+
const numberColor = chalk.hex(hslToHex(Math.round((1 - row.ratio) * 120), 80, 40));
262+
const content =
263+
row.label.padEnd(labelColWidth + gap) +
264+
numberColor(row.percent.padStart(percentColWidth)) +
265+
' '.repeat(gap) +
266+
chalk.hex(colors.text)(row.reset.padStart(resetColWidth));
267+
const leftPad = Math.max(0, width - blockWidth);
268+
lines.push(truncateToWidth(' '.repeat(leftPad) + content, width));
253269
}
254270
return lines;
255271
}
@@ -424,13 +440,7 @@ export class FooterComponent implements Component {
424440
line2 = ' '.repeat(leftPad) + chalk.hex(colors.text)(contextText);
425441
}
426442

427-
const quotaLines = formatQuotaLines(
428-
state.quotas,
429-
contextText,
430-
contextWidth,
431-
width,
432-
colors,
433-
);
443+
const quotaLines = formatQuotaLines(state.quotas, width, colors);
434444
if (quotaLines.length > 0) {
435445
return [
436446
truncateToWidth(line1, width),

apps/kimi-code/test/tui/components/chrome/footer.test.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -113,23 +113,34 @@ describe('FooterComponent', () => {
113113
expect(footer.render(80).length).toBe(2);
114114
});
115115

116-
it('renders managed quota rows aligned under the context parentheses', () => {
116+
it('renders managed quota rows in aligned columns with reset hint', () => {
117117
const state: AppState = {
118118
...appState,
119119
contextUsage: 0.5,
120120
contextTokens: 1_000,
121121
maxContextTokens: 2_000,
122-
quotas: [{ label: 'Weekly limit', used: 25, limit: 100 }],
122+
quotas: [
123+
{ label: 'Weekly limit', used: 41, limit: 100, resetHint: 'resets in 5d 3h' },
124+
{ label: '5H LIMIT', used: 65, limit: 100, resetHint: 'resets in 1h 3m' },
125+
],
123126
};
124127
const footer = new FooterComponent(state);
125128
const lines = footer.render(200);
126129

127-
expect(lines.length).toBe(3);
128-
const contextLine = stripAnsi(lines[1]!);
129-
const quotaLine = stripAnsi(lines[2]!);
130-
expect(quotaLine).toContain('weekly limit');
131-
expect(quotaLine).toContain('(25/100%)');
132-
expect(quotaLine.indexOf('(')).toBe(contextLine.indexOf('('));
130+
expect(lines.length).toBe(4);
131+
const weekLine = stripAnsi(lines[2]!);
132+
const hourLine = stripAnsi(lines[3]!);
133+
134+
expect(weekLine).toContain('weekly limit:');
135+
expect(weekLine).toContain('41%');
136+
expect(weekLine).toContain('(5d, 3h)');
137+
138+
expect(hourLine).toContain('5h limit:');
139+
expect(hourLine).toContain('65%');
140+
expect(hourLine).toContain('(1h, 3m)');
141+
142+
// Both rows should end at the same column (right-aligned block).
143+
expect(weekLine.trimEnd().length).toBe(hourLine.trimEnd().length);
133144
});
134145

135146
it('lowercases quota labels and colors the percentage', () => {
@@ -138,14 +149,15 @@ describe('FooterComponent', () => {
138149
contextUsage: 0,
139150
contextTokens: 1_000_000,
140151
maxContextTokens: 2_000_000,
141-
quotas: [{ label: '5H LIMIT', used: 50, limit: 100 }],
152+
quotas: [{ label: '5H LIMIT', used: 50, limit: 100, resetHint: 'reset' }],
142153
};
143154
const footer = new FooterComponent(state);
144155
const lines = footer.render(120);
145156
const quotaLine = lines[2]!;
146157

147-
expect(stripAnsi(quotaLine)).toContain('5h limit');
148-
expect(stripAnsi(quotaLine)).toContain('(50/100%)');
158+
expect(stripAnsi(quotaLine)).toContain('5h limit:');
159+
expect(stripAnsi(quotaLine)).toContain('50%');
160+
expect(stripAnsi(quotaLine)).toContain('(reset)');
149161
expect(truecolorCodes(quotaLine).size).toBeGreaterThan(0);
150162
});
151163
});

0 commit comments

Comments
 (0)