fix(tui): recompute stale context usage ratio from status update token counts (#3164)

v2 engine status events carry contextTokens/maxContextTokens but never
contextUsage, so appState.contextUsage was only refreshed by getStatus
pulls and then went stale while the token counts kept updating live.
The /usage panel and footer render the ratio as a bar but recompute the
percentage text from the counts, so a stale ratio showed as a bar that
disagreed with the percentage (e.g. bar ~74% next to "18% (180k / 1M)"
after compaction or a model switch).

Recompute the ratio from the post-patch token counts whenever a status
update touches contextTokens or maxContextTokens without carrying an
explicit contextUsage. v1 events carry the ratio and are unaffected.

Co-authored-by: kimi-agent-bot <kimi-agent-bot@users.noreply.github.com>
This commit is contained in:
Kimi Agent 2026-08-26 13:59:24 +08:00 committed by GitHub
parent 6595955b31
commit 41a75adfc7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 100 additions and 1 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
Fix the context usage bar in /usage and the footer showing a stale percentage after the context size or model changes.

View file

@ -712,9 +712,20 @@ export class SessionEventHandler {
this.host.state.appState.swarmMode &&
this.host.state.swarmModeEntry === 'task';
const patch: Partial<AppState> = {};
if (event.contextUsage !== undefined) patch.contextUsage = event.contextUsage;
if (event.contextTokens !== undefined) patch.contextTokens = event.contextTokens;
if (event.maxContextTokens !== undefined) patch.maxContextTokens = event.maxContextTokens;
if (event.contextUsage !== undefined) {
patch.contextUsage = event.contextUsage;
} else if (event.contextTokens !== undefined || event.maxContextTokens !== undefined) {
// v2 status events carry contextTokens/maxContextTokens but never
// contextUsage. Recompute the ratio from the post-patch token counts so
// it cannot go stale and drift from them — the footer and the /usage
// panel bar render this ratio while their texts recompute from the
// counts, so a stale ratio shows as a bar/percentage mismatch.
const tokens = patch.contextTokens ?? this.host.state.appState.contextTokens;
const max = patch.maxContextTokens ?? this.host.state.appState.maxContextTokens;
patch.contextUsage = max > 0 ? tokens / max : 0;
}
if (event.planMode !== undefined) patch.planMode = event.planMode;
if (event.swarmMode !== undefined) patch.swarmMode = event.swarmMode;
if (event.towerMode !== undefined) patch.towerMode = event.towerMode;

View file

@ -5596,6 +5596,89 @@ command = "vim"
expect(stripSgr(renderTranscript(driver))).toContain('LLM not set');
});
it('recomputes contextUsage when a status update carries contextTokens without it', async () => {
const { driver } = await makeDriver();
driver.state.appState.contextTokens = 0;
driver.state.appState.maxContextTokens = 1_000_000;
driver.state.appState.contextUsage = 0.74;
// v2 token-counting events carry contextTokens only; the ratio must be
// recomputed or the footer and /usage bar keep showing the stale value.
driver.sessionEventHandler.handleEvent(
{
type: 'agent.status.updated',
agentId: 'main',
sessionId: 'ses-1',
contextTokens: 180_000,
} as Event,
vi.fn(),
);
expect(driver.state.appState.contextTokens).toBe(180_000);
expect(driver.state.appState.contextUsage).toBeCloseTo(0.18);
});
it('recomputes contextUsage when a status update carries maxContextTokens without it', async () => {
const { driver } = await makeDriver();
driver.state.appState.contextTokens = 180_000;
driver.state.appState.maxContextTokens = 256_000;
driver.state.appState.contextUsage = 180_000 / 256_000;
// v2 profile events carry maxContextTokens only (e.g. a model switch).
driver.sessionEventHandler.handleEvent(
{
type: 'agent.status.updated',
agentId: 'main',
sessionId: 'ses-1',
maxContextTokens: 1_000_000,
} as Event,
vi.fn(),
);
expect(driver.state.appState.maxContextTokens).toBe(1_000_000);
expect(driver.state.appState.contextUsage).toBeCloseTo(0.18);
});
it('keeps an explicit contextUsage from status updates instead of recomputing', async () => {
const { driver } = await makeDriver();
driver.state.appState.contextTokens = 100;
driver.state.appState.maxContextTokens = 1_000_000;
driver.state.appState.contextUsage = 0;
driver.sessionEventHandler.handleEvent(
{
type: 'agent.status.updated',
agentId: 'main',
sessionId: 'ses-1',
contextTokens: 180_000,
maxContextTokens: 1_000_000,
contextUsage: 0.42,
} as Event,
vi.fn(),
);
expect(driver.state.appState.contextUsage).toBe(0.42);
});
it('zeroes contextUsage when a recomputation has no known context window', async () => {
const { driver } = await makeDriver();
driver.state.appState.contextTokens = 180_000;
driver.state.appState.maxContextTokens = 0;
driver.state.appState.contextUsage = 0.74;
driver.sessionEventHandler.handleEvent(
{
type: 'agent.status.updated',
agentId: 'main',
sessionId: 'ses-1',
contextTokens: 190_000,
} as Event,
vi.fn(),
);
expect(driver.state.appState.contextUsage).toBe(0);
});
it('applies the effective thinking effort from status updates', async () => {
const { driver } = await makeDriver();