From a91ba61dd8c2bc7117369547b4cdbbc6d95d333a Mon Sep 17 00:00:00 2001 From: Nikolas Syring Date: Thu, 18 Dec 2025 07:29:29 +0100 Subject: [PATCH] fix: improve telemetry accuracy with modelUsage data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract token counts from modelUsage (per-model stats) instead of basic usage object for accurate values - Add contextWindow from modelUsage to calculate proper context % - Show "X% used" when > 70% free, "X% left" when running low - Color coding: green (ok), yellow (<30% left), red (<15%), pulsing (<5%) - Fix totalTokens undefined error (renamed to contextUsed) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- frontend/src/components/StatusBar.jsx | 26 +++++++++++++--------- frontend/src/hooks/useClaudeSession.js | 30 ++++++++++++++++++++------ 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/StatusBar.jsx b/frontend/src/components/StatusBar.jsx index 1132069..8007de2 100644 --- a/frontend/src/components/StatusBar.jsx +++ b/frontend/src/components/StatusBar.jsx @@ -22,6 +22,7 @@ export function StatusBar({ sessionStats, isProcessing, connected, permissionMod cacheReadTokens = 0, cacheCreationTokens = 0, numTurns = 0, + contextWindow = 200000, isCompacting = false, } = sessionStats || {}; @@ -29,10 +30,12 @@ export function StatusBar({ sessionStats, isProcessing, connected, permissionMod const currentMode = PERMISSION_MODES.find(m => m.value === permissionMode) || PERMISSION_MODES[0]; const ModeIcon = currentMode.icon; - // Calculate total tokens and estimate context usage - const totalTokens = inputTokens + outputTokens; - // Claude has ~200k context, but we show relative usage - const contextPercent = Math.min(100, (inputTokens / 200000) * 100); + // 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); // Format cost const formatCost = (cost) => { @@ -137,7 +140,7 @@ export function StatusBar({ sessionStats, isProcessing, connected, permissionMod {/* Right side: Token usage */}
{/* Token counts */} - {totalTokens > 0 && ( + {contextUsed > 0 && (
In: @@ -156,15 +159,18 @@ export function StatusBar({ sessionStats, isProcessing, connected, permissionMod
)} - {/* Context status - simple text based on remaining context */} + {/* Context status - shows remaining context percentage */}
Context: - {inputTokens > 0 ? ( + {contextUsed > 0 ? ( = 95 ? 'text-red-400 font-medium' : - contextPercent >= 85 ? 'text-yellow-400' : 'text-green-400' + contextRemaining <= 5 ? 'text-red-400 font-medium animate-pulse' : + contextRemaining <= 15 ? 'text-red-400' : + contextRemaining <= 30 ? 'text-yellow-400' : 'text-green-400' }`}> - {contextPercent >= 85 ? `${(100 - contextPercent).toFixed(0)}% left` : 'ok'} + {contextRemaining <= 30 + ? `${contextRemaining.toFixed(0)}% left` + : `${contextPercent.toFixed(0)}% used`} ) : ( ok diff --git a/frontend/src/hooks/useClaudeSession.js b/frontend/src/hooks/useClaudeSession.js index 33d072c..59095d6 100644 --- a/frontend/src/hooks/useClaudeSession.js +++ b/frontend/src/hooks/useClaudeSession.js @@ -32,6 +32,7 @@ export function useClaudeSession() { cacheReadTokens: 0, cacheCreationTokens: 0, numTurns: 0, + contextWindow: 200000, // Default, updated from modelUsage isCompacting: false, }); @@ -361,14 +362,29 @@ export function useClaudeSession() { setIsProcessing(false); // Update session stats from result event - if (event.usage || event.total_cost_usd !== undefined) { + // Claude sends detailed modelUsage with per-model stats including contextWindow + // usage contains: input_tokens (new), cache_read_input_tokens, cache_creation_input_tokens, output_tokens + if (event.usage || event.modelUsage || event.total_cost_usd !== undefined) { + // Get the primary model's usage (usually claude-opus or claude-sonnet) + const modelUsage = event.modelUsage || {}; + const primaryModel = Object.keys(modelUsage).find(k => k.includes('opus') || k.includes('sonnet')) + || 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); + setSessionStats(prev => ({ - totalCost: event.total_cost_usd || prev.totalCost, - inputTokens: event.usage?.input_tokens || prev.inputTokens, - outputTokens: event.usage?.output_tokens || prev.outputTokens, - cacheReadTokens: event.usage?.cache_read_input_tokens || prev.cacheReadTokens, - cacheCreationTokens: event.usage?.cache_creation_input_tokens || prev.cacheCreationTokens, - numTurns: event.num_turns || prev.numTurns, + 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, + numTurns: event.num_turns ?? prev.numTurns, + contextWindow: primaryUsage?.contextWindow ?? prev.contextWindow, isCompacting: false, })); }