From 38bdbafcd0bad7804bcd3f94ad74fdc26e9f4ee9 Mon Sep 17 00:00:00 2001 From: kimi-agent-bot Date: Fri, 21 Aug 2026 09:35:51 +0000 Subject: [PATCH] fix(tui): recompute stale context usage ratio from status update token counts 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. --- .changeset/stale-context-usage-ratio.md | 5 ++ .../tui/controllers/session-event-handler.ts | 13 ++- .../test/tui/kimi-tui-message-flow.test.ts | 83 +++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 .changeset/stale-context-usage-ratio.md diff --git a/.changeset/stale-context-usage-ratio.md b/.changeset/stale-context-usage-ratio.md new file mode 100644 index 0000000000..c4fa21a4f0 --- /dev/null +++ b/.changeset/stale-context-usage-ratio.md @@ -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. diff --git a/apps/kimi-code/src/tui/controllers/session-event-handler.ts b/apps/kimi-code/src/tui/controllers/session-event-handler.ts index 9cc95eba33..0d623d01c2 100644 --- a/apps/kimi-code/src/tui/controllers/session-event-handler.ts +++ b/apps/kimi-code/src/tui/controllers/session-event-handler.ts @@ -712,9 +712,20 @@ export class SessionEventHandler { this.host.state.appState.swarmMode && this.host.state.swarmModeEntry === 'task'; const patch: Partial = {}; - 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; diff --git a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts index 044c71f3b0..0785f35222 100644 --- a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts @@ -5510,6 +5510,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();