fix: Simplify context display to use Claude's reported value

Remove broken token-based context calculation. The StatusBar now only
shows context remaining when Claude reports "Context left until
auto-compact: X%" - which happens when context is actually getting low.

This eliminates confusing/incorrect percentages and only shows relevant
information when needed.

TODO: Test this in a long session to verify the context warning appears
when Claude reports it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-18 09:24:51 +01:00
parent a91ba61dd8
commit a3fcc3cb7f
2 changed files with 56 additions and 48 deletions

View File

@@ -23,6 +23,7 @@ export function StatusBar({ sessionStats, isProcessing, connected, permissionMod
cacheCreationTokens = 0, cacheCreationTokens = 0,
numTurns = 0, numTurns = 0,
contextWindow = 200000, contextWindow = 200000,
contextLeftPercent = null, // From Claude's "Context left until auto-compact: X%" message
isCompacting = false, isCompacting = false,
} = sessionStats || {}; } = sessionStats || {};
@@ -30,12 +31,9 @@ export function StatusBar({ sessionStats, isProcessing, connected, permissionMod
const currentMode = PERMISSION_MODES.find(m => m.value === permissionMode) || PERMISSION_MODES[0]; const currentMode = PERMISSION_MODES.find(m => m.value === permissionMode) || PERMISSION_MODES[0];
const ModeIcon = currentMode.icon; const ModeIcon = currentMode.icon;
// Calculate context usage // Context remaining: Use Claude's reported value if available, otherwise don't show
// inputTokens from modelUsage represents tokens in current context (including cache reads) // Claude only reports this when context is getting low (< ~15%)
// contextWindow is the model's max context size (e.g., 200000 for opus) const contextRemaining = contextLeftPercent;
const contextUsed = inputTokens + outputTokens;
const contextPercent = contextWindow > 0 ? Math.min(100, (contextUsed / contextWindow) * 100) : 0;
const contextRemaining = Math.max(0, 100 - contextPercent);
// Format cost // Format cost
const formatCost = (cost) => { const formatCost = (cost) => {
@@ -139,43 +137,19 @@ export function StatusBar({ sessionStats, isProcessing, connected, permissionMod
{/* Right side: Token usage */} {/* Right side: Token usage */}
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{/* Token counts */} {/* Context status - only shown when Claude reports it (context getting low) */}
{contextUsed > 0 && ( {contextRemaining !== null && (
<div className="flex items-center gap-3 text-dark-400">
<span className="flex items-center gap-1">
<span className="text-dark-500">In:</span>
<span className="text-cyan-400">{formatTokens(inputTokens)}</span>
</span>
<span className="flex items-center gap-1">
<span className="text-dark-500">Out:</span>
<span className="text-green-400">{formatTokens(outputTokens)}</span>
</span>
{cacheReadTokens > 0 && (
<span className="flex items-center gap-1">
<Database className="w-3 h-3 text-purple-400" />
<span className="text-purple-400">{formatTokens(cacheReadTokens)}</span>
</span>
)}
</div>
)}
{/* Context status - shows remaining context percentage */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-dark-500">Context:</span> <span className="text-dark-500">Context:</span>
{contextUsed > 0 ? (
<span className={`${ <span className={`${
contextRemaining <= 5 ? 'text-red-400 font-medium animate-pulse' : contextRemaining <= 5 ? 'text-red-400 font-medium animate-pulse' :
contextRemaining <= 15 ? 'text-red-400' : contextRemaining <= 15 ? 'text-red-400' :
contextRemaining <= 30 ? 'text-yellow-400' : 'text-green-400' contextRemaining <= 30 ? 'text-yellow-400' : 'text-green-400'
}`}> }`}>
{contextRemaining <= 30 {contextRemaining}% left
? `${contextRemaining.toFixed(0)}% left`
: `${contextPercent.toFixed(0)}% used`}
</span> </span>
) : (
<span className="text-green-400">ok</span>
)}
</div> </div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -33,6 +33,7 @@ export function useClaudeSession() {
cacheCreationTokens: 0, cacheCreationTokens: 0,
numTurns: 0, numTurns: 0,
contextWindow: 200000, // Default, updated from modelUsage contextWindow: 200000, // Default, updated from modelUsage
contextLeftPercent: null, // From Claude's "Context left until auto-compact: X%" message
isCompacting: false, isCompacting: false,
}); });
@@ -371,18 +372,38 @@ export function useClaudeSession() {
|| Object.keys(modelUsage)[0]; || Object.keys(modelUsage)[0];
const primaryUsage = primaryModel ? modelUsage[primaryModel] : null; const primaryUsage = primaryModel ? modelUsage[primaryModel] : null;
// Calculate effective input tokens (what's actually in context) // Context calculation: Use top-level usage (aggregated) as primary source
// This is: new input + cache read (cache creation doesn't count towards context limit) // Per GitHub issue anthropics/claude-agent-sdk-typescript#66:
const effectiveInput = (event.usage?.input_tokens || 0) + // Cache tokens DON'T count toward the 200K context window - they're handled separately
(event.usage?.cache_read_input_tokens || 0); // Only input_tokens + output_tokens count toward the context window
// Cache tokens can appear in the millions because they're cumulative across the session
const usage = event.usage || {};
// Prefer top-level usage (aggregated), fallback to primaryUsage (per-model)
const inputTokens = usage.input_tokens ?? primaryUsage?.inputTokens ?? 0;
const cacheRead = usage.cache_read_input_tokens ?? primaryUsage?.cacheReadInputTokens ?? 0;
const cacheCreation = usage.cache_creation_input_tokens ?? primaryUsage?.cacheCreationInputTokens ?? 0;
const outputTokens = usage.output_tokens ?? primaryUsage?.outputTokens ?? 0;
// Context = input + output tokens ONLY (cache tokens are separate)
const totalContextUsed = inputTokens + outputTokens;
console.log('Result event stats:', {
inputTokens,
cacheRead,
cacheCreation,
totalContextUsed,
outputTokens,
contextWindow: primaryUsage?.contextWindow
});
setSessionStats(prev => ({ setSessionStats(prev => ({
totalCost: event.total_cost_usd ?? prev.totalCost, totalCost: event.total_cost_usd ?? prev.totalCost,
// Use modelUsage for accurate per-turn counts, fallback to usage // Context = input + output tokens only (cache tokens don't count toward window)
inputTokens: primaryUsage?.inputTokens ?? effectiveInput ?? prev.inputTokens, inputTokens: totalContextUsed,
outputTokens: primaryUsage?.outputTokens ?? event.usage?.output_tokens ?? prev.outputTokens, outputTokens: outputTokens,
cacheReadTokens: primaryUsage?.cacheReadInputTokens ?? event.usage?.cache_read_input_tokens ?? prev.cacheReadTokens, cacheReadTokens: cacheRead,
cacheCreationTokens: primaryUsage?.cacheCreationInputTokens ?? event.usage?.cache_creation_input_tokens ?? prev.cacheCreationTokens, cacheCreationTokens: cacheCreation,
numTurns: event.num_turns ?? prev.numTurns, numTurns: event.num_turns ?? prev.numTurns,
contextWindow: primaryUsage?.contextWindow ?? prev.contextWindow, contextWindow: primaryUsage?.contextWindow ?? prev.contextWindow,
isCompacting: false, isCompacting: false,
@@ -390,10 +411,23 @@ export function useClaudeSession() {
} }
} else if (event.type === 'system' && event.subtype === 'result') { } else if (event.type === 'system' && event.subtype === 'result') {
setIsProcessing(false); setIsProcessing(false);
} else if (event.type === 'system' && event.message?.includes?.('compact')) { } else if (event.type === 'system' && event.message) {
// Parse "Context left until auto-compact: X%" message
const contextMatch = event.message.match(/Context left until auto-compact:\s*(\d+)%/i);
if (contextMatch) {
const contextLeft = parseInt(contextMatch[1], 10);
console.log('Context left from Claude:', contextLeft + '%');
setSessionStats(prev => ({
...prev,
contextLeftPercent: contextLeft,
isCompacting: false,
}));
}
// Detect compacting // Detect compacting
if (event.message.includes('compact')) {
setSessionStats(prev => ({ ...prev, isCompacting: true })); setSessionStats(prev => ({ ...prev, isCompacting: true }));
} }
}
}, []); }, []);
const loadHistory = useCallback(async (project, host = null) => { const loadHistory = useCallback(async (project, host = null) => {