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();