diff --git a/frontend/src/components/StatusBar.jsx b/frontend/src/components/StatusBar.jsx
index 8007de2..9765531 100644
--- a/frontend/src/components/StatusBar.jsx
+++ b/frontend/src/components/StatusBar.jsx
@@ -23,6 +23,7 @@ export function StatusBar({ sessionStats, isProcessing, connected, permissionMod
cacheCreationTokens = 0,
numTurns = 0,
contextWindow = 200000,
+ contextLeftPercent = null, // From Claude's "Context left until auto-compact: X%" message
isCompacting = false,
} = 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 ModeIcon = currentMode.icon;
- // Calculate context usage
- // inputTokens from modelUsage represents tokens in current context (including cache reads)
- // contextWindow is the model's max context size (e.g., 200000 for opus)
- const contextUsed = inputTokens + outputTokens;
- const contextPercent = contextWindow > 0 ? Math.min(100, (contextUsed / contextWindow) * 100) : 0;
- const contextRemaining = Math.max(0, 100 - contextPercent);
+ // Context remaining: Use Claude's reported value if available, otherwise don't show
+ // Claude only reports this when context is getting low (< ~15%)
+ const contextRemaining = contextLeftPercent;
// Format cost
const formatCost = (cost) => {
@@ -139,43 +137,19 @@ export function StatusBar({ sessionStats, isProcessing, connected, permissionMod
{/* Right side: Token usage */}
- {/* Token counts */}
- {contextUsed > 0 && (
-
-
- In:
- {formatTokens(inputTokens)}
-
-
- Out:
- {formatTokens(outputTokens)}
-
- {cacheReadTokens > 0 && (
-
-
- {formatTokens(cacheReadTokens)}
-
- )}
-
- )}
-
- {/* Context status - shows remaining context percentage */}
-
-
Context:
- {contextUsed > 0 ? (
+ {/* Context status - only shown when Claude reports it (context getting low) */}
+ {contextRemaining !== null && (
+
+ Context:
- {contextRemaining <= 30
- ? `${contextRemaining.toFixed(0)}% left`
- : `${contextPercent.toFixed(0)}% used`}
+ {contextRemaining}% left
- ) : (
- ok
- )}
-
+
+ )}
diff --git a/frontend/src/hooks/useClaudeSession.js b/frontend/src/hooks/useClaudeSession.js
index 59095d6..d2eee62 100644
--- a/frontend/src/hooks/useClaudeSession.js
+++ b/frontend/src/hooks/useClaudeSession.js
@@ -33,6 +33,7 @@ export function useClaudeSession() {
cacheCreationTokens: 0,
numTurns: 0,
contextWindow: 200000, // Default, updated from modelUsage
+ contextLeftPercent: null, // From Claude's "Context left until auto-compact: X%" message
isCompacting: false,
});
@@ -371,18 +372,38 @@ export function useClaudeSession() {
|| Object.keys(modelUsage)[0];
const primaryUsage = primaryModel ? modelUsage[primaryModel] : null;
- // Calculate effective input tokens (what's actually in context)
- // This is: new input + cache read (cache creation doesn't count towards context limit)
- const effectiveInput = (event.usage?.input_tokens || 0) +
- (event.usage?.cache_read_input_tokens || 0);
+ // Context calculation: Use top-level usage (aggregated) as primary source
+ // Per GitHub issue anthropics/claude-agent-sdk-typescript#66:
+ // Cache tokens DON'T count toward the 200K context window - they're handled separately
+ // 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 => ({
totalCost: event.total_cost_usd ?? prev.totalCost,
- // Use modelUsage for accurate per-turn counts, fallback to usage
- inputTokens: primaryUsage?.inputTokens ?? effectiveInput ?? prev.inputTokens,
- outputTokens: primaryUsage?.outputTokens ?? event.usage?.output_tokens ?? prev.outputTokens,
- cacheReadTokens: primaryUsage?.cacheReadInputTokens ?? event.usage?.cache_read_input_tokens ?? prev.cacheReadTokens,
- cacheCreationTokens: primaryUsage?.cacheCreationInputTokens ?? event.usage?.cache_creation_input_tokens ?? prev.cacheCreationTokens,
+ // Context = input + output tokens only (cache tokens don't count toward window)
+ inputTokens: totalContextUsed,
+ outputTokens: outputTokens,
+ cacheReadTokens: cacheRead,
+ cacheCreationTokens: cacheCreation,
numTurns: event.num_turns ?? prev.numTurns,
contextWindow: primaryUsage?.contextWindow ?? prev.contextWindow,
isCompacting: false,
@@ -390,9 +411,22 @@ export function useClaudeSession() {
}
} else if (event.type === 'system' && event.subtype === 'result') {
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
- setSessionStats(prev => ({ ...prev, isCompacting: true }));
+ if (event.message.includes('compact')) {
+ setSessionStats(prev => ({ ...prev, isCompacting: true }));
+ }
}
}, []);