fix: align context usage with claude code

This commit is contained in:
Ronald Eddings
2026-01-11 06:55:17 -06:00
parent e0390b270d
commit c93ed9292d
8 changed files with 88 additions and 60 deletions

View File

@@ -99,7 +99,8 @@ describe('estimateContextUsage', () => {
contextWindow: 200000,
});
const result = estimateContextUsage(stats, 'claude-code');
expect(result).toBe(100);
// Output tokens excluded; 150k / 200k = 75%
expect(result).toBe(75);
});
});
@@ -113,7 +114,7 @@ describe('estimateContextUsage', () => {
it('should use codex default context window (200k)', () => {
const stats = createStats({ contextWindow: 0 });
const result = estimateContextUsage(stats, 'codex');
expect(result).toBe(8);
expect(result).toBe(5);
});
it('should use opencode default context window (128k)', () => {

View File

@@ -642,7 +642,7 @@ describe('HistoryDetailModal', () => {
usageStats: {
inputTokens: 74000,
outputTokens: 1000,
cacheReadInputTokens: 0,
cacheReadInputTokens: 1000,
cacheCreationInputTokens: 0,
contextWindow: 100000,
totalCostUsd: 0,
@@ -652,7 +652,7 @@ describe('HistoryDetailModal', () => {
/>
);
// (74000 + 1000) / 100000 = 75%
// (74000 + 1000 cache read) / 100000 = 75%
expect(screen.getByText('75%')).toBeInTheDocument();
});

View File

@@ -1794,8 +1794,8 @@ describe('MainPanel', () => {
createdAt: Date.now(),
usageStats: {
inputTokens: 50000,
outputTokens: 25000,
cacheReadInputTokens: 0,
outputTokens: 0,
cacheReadInputTokens: 25000,
cacheCreationInputTokens: 0,
totalCostUsd: 0.05,
contextWindow: 200000,
@@ -1806,7 +1806,7 @@ describe('MainPanel', () => {
render(<MainPanel {...defaultProps} activeSession={session} getContextColor={getContextColor} />);
// Context usage should be (50000 + 25000) / 200000 * 100 = 37.5%
// Context usage should be (50000 + 25000 cache read) / 200000 * 100 = 37.5%
expect(getContextColor).toHaveBeenCalledWith(38, theme); // Rounded to 38
});
});

View File

@@ -61,8 +61,8 @@ const createTestTab = (overrides: Partial<AITab> = {}): AITab => ({
usageStats: {
inputTokens: 1000,
outputTokens: 500,
cacheReadTokens: 0,
cacheWriteTokens: 0,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
totalCostUsd: 0.05,
contextWindow: 200000,
},
@@ -104,8 +104,8 @@ describe('TabSwitcherModal', () => {
usageStats: {
inputTokens: 500,
outputTokens: 200,
cacheReadTokens: 0,
cacheWriteTokens: 0,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
totalCostUsd: 0.01,
contextWindow: 200000,
},
@@ -132,8 +132,8 @@ describe('TabSwitcherModal', () => {
usageStats: {
inputTokens: 5000,
outputTokens: 3500,
cacheReadTokens: 0,
cacheWriteTokens: 0,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
totalCostUsd: 0.10,
contextWindow: 200000,
},
@@ -160,8 +160,8 @@ describe('TabSwitcherModal', () => {
usageStats: {
inputTokens: 700,
outputTokens: 300,
cacheReadTokens: 0,
cacheWriteTokens: 0,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
totalCostUsd: 0.02,
contextWindow: 200000,
},
@@ -189,8 +189,8 @@ describe('TabSwitcherModal', () => {
usageStats: {
inputTokens: 100,
outputTokens: 50,
cacheReadTokens: 0,
cacheWriteTokens: 0,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
totalCostUsd: 0,
contextWindow: 200000,
},
@@ -216,8 +216,8 @@ describe('TabSwitcherModal', () => {
usageStats: {
inputTokens: 100,
outputTokens: 50,
cacheReadTokens: 0,
cacheWriteTokens: 0,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
totalCostUsd: 0.005,
contextWindow: 200000,
},
@@ -243,8 +243,8 @@ describe('TabSwitcherModal', () => {
usageStats: {
inputTokens: 1000,
outputTokens: 500,
cacheReadTokens: 0,
cacheWriteTokens: 0,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
totalCostUsd: 1.23,
contextWindow: 200000,
},
@@ -501,8 +501,8 @@ describe('TabSwitcherModal', () => {
usageStats: {
inputTokens: 1000,
outputTokens: 500,
cacheReadTokens: 0,
cacheWriteTokens: 0,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
totalCostUsd: 0.05,
contextWindow: 0,
},
@@ -527,10 +527,10 @@ describe('TabSwitcherModal', () => {
it('calculates correct percentage', () => {
const tab = createTestTab({
usageStats: {
inputTokens: 10000,
inputTokens: 20000,
outputTokens: 10000,
cacheReadTokens: 0,
cacheWriteTokens: 0,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
totalCostUsd: 0.50,
contextWindow: 100000,
},
@@ -556,9 +556,9 @@ describe('TabSwitcherModal', () => {
const tab = createTestTab({
usageStats: {
inputTokens: 150000,
outputTokens: 100000,
cacheReadTokens: 0,
cacheWriteTokens: 0,
outputTokens: 0,
cacheReadInputTokens: 100000,
cacheCreationInputTokens: 0,
totalCostUsd: 5.00,
contextWindow: 200000,
},
@@ -810,8 +810,8 @@ describe('TabSwitcherModal', () => {
usageStats: {
inputTokens: 10000,
outputTokens: 10000,
cacheReadTokens: 0,
cacheWriteTokens: 0,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
totalCostUsd: 0.50,
contextWindow: 100000,
},
@@ -842,10 +842,10 @@ describe('TabSwitcherModal', () => {
it('applies color based on percentage (success for low)', () => {
const tab = createTestTab({
usageStats: {
inputTokens: 5000,
inputTokens: 10000,
outputTokens: 5000,
cacheReadTokens: 0,
cacheWriteTokens: 0,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
totalCostUsd: 0.25,
contextWindow: 200000,
},

View File

@@ -94,8 +94,8 @@ describe('extractTabContext', () => {
usageStats: {
inputTokens: 100,
outputTokens: 200,
cacheReadTokens: 50,
cacheCreationTokens: 0,
cacheReadInputTokens: 50,
cacheCreationInputTokens: 0,
costUsd: 0.01,
},
});
@@ -424,15 +424,15 @@ describe('estimateTokenCount', () => {
usageStats: {
inputTokens: 500,
outputTokens: 1000,
cacheReadTokens: 0,
cacheCreationTokens: 0,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 200,
costUsd: 0.05,
},
};
const tokens = estimateTokenCount(context);
expect(tokens).toBe(1500);
expect(tokens).toBe(700); // input + cacheCreation + cacheRead
});
it('should estimate from log content when no usage stats', () => {
@@ -614,7 +614,13 @@ describe('calculateTotalTokens', () => {
name: 'Context 1',
logs: [],
agentType: 'claude-code',
usageStats: { inputTokens: 100, outputTokens: 200, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0 },
usageStats: {
inputTokens: 100,
outputTokens: 200,
cacheReadInputTokens: 50,
cacheCreationInputTokens: 25,
costUsd: 0,
},
},
{
type: 'tab',
@@ -623,13 +629,20 @@ describe('calculateTotalTokens', () => {
name: 'Context 2',
logs: [],
agentType: 'claude-code',
usageStats: { inputTokens: 300, outputTokens: 400, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0 },
usageStats: {
inputTokens: 300,
outputTokens: 400,
cacheReadInputTokens: 75,
cacheCreationInputTokens: 25,
costUsd: 0,
},
},
];
const total = calculateTotalTokens(contexts);
expect(total).toBe(1000); // (100+200) + (300+400)
// input + cacheCreation + cacheRead for each context
expect(total).toBe(575); // (100+25+50) + (300+25+75)
});
});
@@ -643,7 +656,13 @@ describe('getContextSummary', () => {
name: 'Context 1',
logs: [createMockLog(), createMockLog()],
agentType: 'claude-code',
usageStats: { inputTokens: 100, outputTokens: 100, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0 },
usageStats: {
inputTokens: 100,
outputTokens: 100,
cacheReadInputTokens: 50,
cacheCreationInputTokens: 25,
costUsd: 0,
},
},
{
type: 'session',
@@ -652,7 +671,13 @@ describe('getContextSummary', () => {
name: 'Context 2',
logs: [createMockLog(), createMockLog(), createMockLog()],
agentType: 'opencode',
usageStats: { inputTokens: 200, outputTokens: 200, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0 },
usageStats: {
inputTokens: 200,
outputTokens: 200,
cacheReadInputTokens: 75,
cacheCreationInputTokens: 25,
costUsd: 0,
},
},
];
@@ -660,7 +685,7 @@ describe('getContextSummary', () => {
expect(summary.totalSources).toBe(2);
expect(summary.totalLogs).toBe(5);
expect(summary.estimatedTokens).toBe(600);
expect(summary.estimatedTokens).toBe(475);
expect(summary.byAgent['claude-code']).toBe(1);
expect(summary.byAgent['opencode']).toBe(1);
});

View File

@@ -72,13 +72,13 @@ describe('estimateContextUsage', () => {
it('should use claude default context window (200k)', () => {
const stats = createStats({ contextWindow: 0 });
const result = estimateContextUsage(stats, 'claude');
expect(result).toBe(8);
expect(result).toBe(5);
});
it('should use codex default context window (200k)', () => {
const stats = createStats({ contextWindow: 0 });
const result = estimateContextUsage(stats, 'codex');
expect(result).toBe(8);
expect(result).toBe(5);
});
it('should use opencode default context window (128k)', () => {
@@ -91,7 +91,7 @@ describe('estimateContextUsage', () => {
it('should use aider default context window (128k)', () => {
const stats = createStats({ contextWindow: 0 });
const result = estimateContextUsage(stats, 'aider');
expect(result).toBe(12);
expect(result).toBe(8);
});
it('should return null for terminal agent', () => {
@@ -151,7 +151,7 @@ describe('estimateContextUsage', () => {
const stats = createStats({ contextWindow: -100 });
const result = estimateContextUsage(stats, 'claude-code');
// Should use fallback since contextWindow is invalid
expect(result).toBe(8);
expect(result).toBe(5);
});
it('should handle undefined context window', () => {
@@ -160,7 +160,7 @@ describe('estimateContextUsage', () => {
stats.contextWindow = undefined;
const result = estimateContextUsage(stats, 'claude-code');
// Should use fallback
expect(result).toBe(8);
expect(result).toBe(5);
});
it('should handle very large token counts', () => {

View File

@@ -417,7 +417,7 @@ describe('SessionStatusBanner', () => {
it('shows warning color (yellow) for usage 70-89%', () => {
const usageStats = createUsageStats({
inputTokens: 100000,
inputTokens: 160000,
outputTokens: 60000,
contextWindow: 200000,
});
@@ -464,7 +464,7 @@ describe('SessionStatusBanner', () => {
it('shows warning color at exactly 70%', () => {
const usageStats = createUsageStats({
inputTokens: 70000,
inputTokens: 140000,
outputTokens: 70000,
contextWindow: 200000,
});
@@ -770,7 +770,7 @@ describe('SessionStatusBanner', () => {
describe('ContextUsageBar component', () => {
it('renders progressbar with correct ARIA attributes', () => {
const usageStats = createUsageStats({
inputTokens: 50000,
inputTokens: 100000,
outputTokens: 50000,
contextWindow: 200000,
});
@@ -786,7 +786,7 @@ describe('SessionStatusBanner', () => {
it('has accessible aria-label', () => {
const usageStats = createUsageStats({
inputTokens: 50000,
inputTokens: 100000,
outputTokens: 50000,
contextWindow: 200000,
});
@@ -799,7 +799,7 @@ describe('SessionStatusBanner', () => {
it('has descriptive title', () => {
const usageStats = createUsageStats({
inputTokens: 50000,
inputTokens: 100000,
outputTokens: 50000,
contextWindow: 200000,
});
@@ -1285,7 +1285,7 @@ describe('SessionStatusBanner', () => {
// Tokens
expect(screen.getByText('7.0K')).toBeInTheDocument();
// Context usage
expect(screen.getByText('4%')).toBeInTheDocument();
expect(screen.getByText('3%')).toBeInTheDocument();
// Last response section
expect(screen.getByText(/5m ago/)).toBeInTheDocument();
});
@@ -1443,7 +1443,7 @@ describe('SessionStatusBanner', () => {
expect(screen.getByText('AI')).toBeInTheDocument();
expect(screen.getByText('$2.50')).toBeInTheDocument();
expect(screen.getByText('180.0K')).toBeInTheDocument();
expect(screen.getByText('90%')).toBeInTheDocument();
expect(screen.getByText('75%')).toBeInTheDocument();
expect(screen.getByText('1:05')).toBeInTheDocument();
expect(screen.getByLabelText('AI is thinking')).toBeInTheDocument();
});

View File

@@ -343,9 +343,11 @@ export function HistoryDetailModal({
</span>
</div>
{(() => {
// Context usage = (input + output + cache read) / context window
// Cache read tokens represent the conversation history sent with each request
const contextTokens = entry.usageStats!.inputTokens + entry.usageStats!.outputTokens + (entry.usageStats!.cacheReadInputTokens || 0);
// Context usage = (input + cache creation + cache read) / context window
const contextTokens =
entry.usageStats!.inputTokens +
(entry.usageStats!.cacheCreationInputTokens || 0) +
(entry.usageStats!.cacheReadInputTokens || 0);
const contextUsage = Math.min(100, Math.round((contextTokens / entry.usageStats!.contextWindow) * 100));
return (
<div className="flex flex-col gap-1">