Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/stale-context-usage-ratio.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 12 additions & 1 deletion apps/kimi-code/src/tui/controllers/session-event-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
83 changes: 83 additions & 0 deletions apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading