Feature: Kontext-Auslastung im Footer (X% ctx)

- Bridge: Token-Berechnung inkl. Cache (input + cache_read + cache_creation)
- Store: contextUsage + contextPercent (derived)
- Layout: Farbcodierte Anzeige (grün/gelb/rot bei 60%/80%)
- Tooltip zeigt absolute Token-Zahlen

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-15 13:40:34 +02:00
parent 48fd61fd01
commit f191cd062c
4 changed files with 65 additions and 7 deletions

View file

@ -547,25 +547,39 @@ async function sendMessage(message, requestId, model = null, contextOverride = n
case 'result': { case 'result': {
// Endergebnis // Endergebnis
const durationMs = Date.now() - startTime; const durationMs = Date.now() - startTime;
const inputTokens = event.usage?.input_tokens || 0;
const outputTokens = event.usage?.output_tokens || 0; // Token-Zählung: input_tokens + cache_read + cache_creation = tatsächlicher Kontext
const usage = event.usage || {};
const inputTokens = usage.input_tokens || 0;
const cacheRead = usage.cache_read_input_tokens || 0;
const cacheCreation = usage.cache_creation_input_tokens || 0;
const contextTokens = inputTokens + cacheRead + cacheCreation;
const outputTokens = usage.output_tokens || 0;
const cost = event.total_cost_usd || 0; const cost = event.total_cost_usd || 0;
sendEvent('result', { sendEvent('result', {
text: fullText, text: fullText,
cost, cost,
tokens: { input: inputTokens, output: outputTokens }, tokens: {
input: contextTokens, // Gesamter Kontext (inkl. Cache)
output: outputTokens,
raw_input: inputTokens, // Nur neue Token (für Debug)
cache_read: cacheRead,
cache_creation: cacheCreation,
},
session_id: event.session_id || '', session_id: event.session_id || '',
duration_ms: durationMs, duration_ms: durationMs,
model: usedModel, model: usedModel,
}); });
// Monitor: API-Response // Monitor: API-Response
const tokenK = ((inputTokens + outputTokens) / 1000).toFixed(1); const tokenK = ((contextTokens + outputTokens) / 1000).toFixed(1);
sendMonitorEvent('api', `${usedModel} [${durationMs}ms] ${tokenK}k tok $${cost.toFixed(4)}`, { sendMonitorEvent('api', `${usedModel} [${durationMs}ms] ${tokenK}k ctx $${cost.toFixed(4)}`, {
model: usedModel, model: usedModel,
inputTokens, contextTokens,
outputTokens, outputTokens,
cacheRead,
cacheCreation,
cost, cost,
sessionId: event.session_id, sessionId: event.session_id,
}, { durationMs }); }, { durationMs });

View file

@ -70,6 +70,19 @@ export const sessionStats = writable({
messageCount: 0, messageCount: 0,
}); });
// Kontext-Auslastung (aktueller API-Call)
// inputTokens = was Claude bei diesem Request "gelesen" hat (System + Konversation)
export const contextUsage = writable({
inputTokens: 0, // Aktuelle Kontext-Tokens
outputTokens: 0, // Tokens der letzten Antwort
contextLimit: 200000, // Claude 3.5/Opus Context Window
});
// Abgeleitet: Prozent der Kontext-Auslastung
export const contextPercent = derived(contextUsage, ($ctx) =>
Math.round(($ctx.inputTokens / $ctx.contextLimit) * 100)
);
// Sticky Context Status (beim App-Start geladen) // Sticky Context Status (beim App-Start geladen)
export interface StickyContextInfo { export interface StickyContextInfo {
loaded: boolean; loaded: boolean;

View file

@ -18,6 +18,7 @@ import {
clearAll, clearAll,
currentModel, currentModel,
sessionStats, sessionStats,
contextUsage,
currentSessionId, currentSessionId,
messageToDb, messageToDb,
addMonitorEvent, addMonitorEvent,
@ -319,6 +320,15 @@ export async function initEventListeners(): Promise<void> {
totalCost: s.totalCost + (cost || 0), totalCost: s.totalCost + (cost || 0),
messageCount: s.messageCount + 1, messageCount: s.messageCount + 1,
})); }));
// Kontext-Auslastung aktualisieren (input_tokens = aktuelle Kontext-Größe)
if (tokens?.input) {
contextUsage.update((ctx) => ({
...ctx,
inputTokens: tokens.input,
outputTokens: tokens.output || 0,
}));
}
} }
}) })
); );

View file

@ -2,7 +2,7 @@
import '../app.css'; import '../app.css';
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { isProcessing, agentCount, currentModel, sessionStats, initEventListeners, cleanupEventListeners, currentSessionId, setMessagesFromDb, stickyContextInfo, agentMode, type DbMessage, type StickyContextInfo, type AgentMode } from '$lib/stores'; import { isProcessing, agentCount, currentModel, sessionStats, contextPercent, contextUsage, initEventListeners, cleanupEventListeners, currentSessionId, setMessagesFromDb, stickyContextInfo, agentMode, type DbMessage, type StickyContextInfo, type AgentMode } from '$lib/stores';
import StopButton from '$lib/components/StopButton.svelte'; import StopButton from '$lib/components/StopButton.svelte';
// Session-Typ vom Backend // Session-Typ vom Backend
@ -178,6 +178,12 @@
</span> </span>
<span class="sep">|</span> <span class="sep">|</span>
{/if} {/if}
{#if $contextUsage.inputTokens > 0}
<span class="context-percent" class:warning={$contextPercent > 60} class:danger={$contextPercent > 80} title="{formatTokens($contextUsage.inputTokens)} von {formatTokens($contextUsage.contextLimit)} Token">
{$contextPercent}% ctx
</span>
<span class="sep">|</span>
{/if}
<span>Token: {formatTokens($sessionStats.totalTokensIn)} in / {formatTokens($sessionStats.totalTokensOut)} out</span> <span>Token: {formatTokens($sessionStats.totalTokensIn)} in / {formatTokens($sessionStats.totalTokensOut)} out</span>
<span class="sep">|</span> <span class="sep">|</span>
<span>Kosten: {formatCost($sessionStats.totalCost)}</span> <span>Kosten: {formatCost($sessionStats.totalCost)}</span>
@ -311,6 +317,21 @@
cursor: help; cursor: help;
} }
.footer-stats .context-percent {
color: #22c55e;
font-weight: 600;
cursor: help;
}
.footer-stats .context-percent.warning {
color: #eab308;
}
.footer-stats .context-percent.danger {
color: #ef4444;
animation: pulse 1.5s ease-in-out infinite;
}
.footer-stats .mode-badge { .footer-stats .mode-badge {
font-weight: 600; font-weight: 600;
cursor: help; cursor: help;