Statusleiste mit Token/Kosten + Modell-Badge + STOPP funktionsfähig

- Titlebar: Token in/out, Kosten, Modell-Badge (z.B. "Opus 4.6")
- sessionStats Store: kumulierte Token/Kosten pro Session
- STOPP-Button ruft invoke('stop_all_agents') auf
- Escape-Hotkey zum Stoppen
- Kompakteres Layout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-13 22:14:46 +02:00
parent 3cbf77e832
commit e09fb8815c
3 changed files with 91 additions and 74 deletions

View file

@ -50,6 +50,15 @@ export const permissions = writable<Permission[]>([]);
export const isProcessing = writable(false); export const isProcessing = writable(false);
export const currentInput = writable(''); export const currentInput = writable('');
export const selectedAgentId = writable<string | null>(null); export const selectedAgentId = writable<string | null>(null);
export const currentModel = writable('');
// Session-Statistiken (kumuliert)
export const sessionStats = writable({
totalTokensIn: 0,
totalTokensOut: 0,
totalCost: 0,
messageCount: 0,
});
// Abgeleitete Stores // Abgeleitete Stores
export const activeAgents = derived(agents, ($agents) => export const activeAgents = derived(agents, ($agents) =>

View file

@ -12,7 +12,9 @@ import {
updateAgentStatus, updateAgentStatus,
addToolCall, addToolCall,
completeToolCall, completeToolCall,
clearAll clearAll,
currentModel,
sessionStats
} from './app'; } from './app';
// Event-Typen vom Backend // Event-Typen vom Backend
@ -168,6 +170,17 @@ export async function initEventListeners(): Promise<void> {
messages.update((msgs) => messages.update((msgs) =>
msgs.map((m) => m.id === streamingMessageId ? { ...m, model } : m) msgs.map((m) => m.id === streamingMessageId ? { ...m, model } : m)
); );
currentModel.set(model);
}
// Session-Statistiken aktualisieren
if (tokens || cost) {
sessionStats.update((s) => ({
totalTokensIn: s.totalTokensIn + (tokens?.input || 0),
totalTokensOut: s.totalTokensOut + (tokens?.output || 0),
totalCost: s.totalCost + (cost || 0),
messageCount: s.messageCount + 1,
}));
} }
}) })
); );

View file

@ -2,10 +2,9 @@
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, initEventListeners, cleanupEventListeners } from '$lib/stores'; import { isProcessing, agentCount, currentModel, sessionStats, initEventListeners, cleanupEventListeners } from '$lib/stores';
import StopButton from '$lib/components/StopButton.svelte'; import StopButton from '$lib/components/StopButton.svelte';
// Events beim Laden initialisieren
onMount(async () => { onMount(async () => {
await initEventListeners(); await initEventListeners();
}); });
@ -14,9 +13,7 @@
await cleanupEventListeners(); await cleanupEventListeners();
}); });
// STOPP-Funktion
async function handleStop() { async function handleStop() {
console.log('STOPP gedrückt — breche alle Agents ab');
try { try {
await invoke('stop_all_agents'); await invoke('stop_all_agents');
} catch (err) { } catch (err) {
@ -25,12 +22,23 @@
$isProcessing = false; $isProcessing = false;
} }
// Hotkey: Escape zum Stoppen
function handleKeydown(event: KeyboardEvent) { function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape' && $isProcessing) { if (event.key === 'Escape' && $isProcessing) {
handleStop(); handleStop();
} }
} }
function formatCost(usd: number): string {
if (usd === 0) return '$0';
if (usd < 0.01) return `$${usd.toFixed(4)}`;
return `$${usd.toFixed(2)}`;
}
function formatTokens(n: number): string {
if (n === 0) return '0';
if (n < 1000) return String(n);
return `${(n / 1000).toFixed(1)}k`;
}
</script> </script>
<svelte:window on:keydown={handleKeydown} /> <svelte:window on:keydown={handleKeydown} />
@ -39,38 +47,36 @@
<!-- Titelleiste --> <!-- Titelleiste -->
<header class="titlebar"> <header class="titlebar">
<div class="titlebar-left"> <div class="titlebar-left">
<span class="app-icon">🤖</span>
<h1>Claude Desktop</h1> <h1>Claude Desktop</h1>
</div> </div>
<div class="titlebar-center"> <div class="titlebar-center">
{#if $isProcessing} {#if $isProcessing}
<span class="status-indicator active"></span> <span class="status-dot active"></span>
<span>Arbeitet...</span> <span>Arbeitet...</span>
{:else} {:else}
<span class="status-indicator idle"></span> <span class="status-dot idle"></span>
<span>Bereit</span> <span>Bereit</span>
{/if} {/if}
{#if $currentModel}
<span class="model-badge">{$currentModel.replace('claude-', '').replace(/-\d{8}$/, '').replace(/(\D)-(\d)/g, '$1 $2').replace(/(\d)-(\d)/g, '$1.$2').replace(/\b[a-z]/g, c => c.toUpperCase())}</span>
{/if}
</div> </div>
<div class="titlebar-right"> <div class="titlebar-right">
<span class="agent-stats"> <span>{formatTokens($sessionStats.totalTokensIn)} in</span>
{$agentCount.active} aktiv | {$agentCount.waiting} wartend | {$agentCount.idle} idle <span class="sep">|</span>
</span> <span>{formatTokens($sessionStats.totalTokensOut)} out</span>
<span class="sep">|</span>
<span>{formatCost($sessionStats.totalCost)}</span>
</div> </div>
</header> </header>
<!-- Haupt-Inhalt mit 3 Panels -->
<main class="main-content"> <main class="main-content">
<slot /> <slot />
</main> </main>
<!-- STOPP-Button (immer sichtbar, unten) --> <!-- STOPP-Footer -->
<footer class="stop-footer" class:active={$isProcessing}> <footer class="stop-footer" class:active={$isProcessing}>
<StopButton on:click={handleStop} disabled={!$isProcessing} /> <StopButton on:click={handleStop} disabled={!$isProcessing} />
<div class="footer-stats">
<span>Token: 0 / 200k</span>
<span>|</span>
<span>CPU: 0%</span>
</div>
</footer> </footer>
</div> </div>
@ -82,93 +88,82 @@
background: var(--bg-primary); background: var(--bg-primary);
} }
/* Titelleiste */
.titlebar { .titlebar {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: var(--spacing-sm) var(--spacing-md); padding: var(--spacing-xs) var(--spacing-md);
background: var(--bg-secondary); background: var(--bg-secondary);
border-bottom: 1px solid var(--bg-tertiary); border-bottom: 1px solid var(--border);
-webkit-app-region: drag;
user-select: none; user-select: none;
} height: 36px;
.titlebar-left {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.app-icon {
font-size: 1.5rem;
} }
.titlebar h1 { .titlebar h1 {
font-size: 1rem; font-size: 0.85rem;
font-weight: 600; font-weight: 600;
color: var(--text-heading);
} }
.titlebar-center { .titlebar-center {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-xs); gap: var(--spacing-xs);
font-size: 0.875rem;
color: var(--text-secondary);
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
}
.status-indicator.active {
background: var(--success);
animation: pulse 1.5s ease-in-out infinite;
}
.status-indicator.idle {
background: var(--text-secondary);
}
.titlebar-right {
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text-secondary); color: var(--text-secondary);
} }
/* Haupt-Inhalt */ .status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
}
.status-dot.active {
background: var(--success);
animation: pulse 1.5s ease-in-out infinite;
}
.status-dot.idle {
background: var(--text-secondary);
}
.model-badge {
padding: 1px 6px;
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
font-size: 0.65rem;
color: var(--accent);
font-weight: 600;
}
.titlebar-right {
display: flex;
gap: var(--spacing-xs);
font-size: 0.65rem;
color: var(--text-secondary);
font-family: var(--font-mono);
}
.sep {
opacity: 0.3;
}
.main-content { .main-content {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
} }
/* STOPP-Footer */
.stop-footer { .stop-footer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: var(--spacing-xs); padding: var(--spacing-xs) var(--spacing-md);
padding: var(--spacing-sm) var(--spacing-md);
background: var(--bg-secondary); background: var(--bg-secondary);
border-top: 1px solid var(--bg-tertiary); border-top: 1px solid var(--border);
transition: border-color 0.3s ease;
} }
.stop-footer.active { .stop-footer.active {
border-top: 2px solid var(--accent); border-top: 2px solid var(--error);
animation: glow 1.5s ease-in-out infinite;
}
@keyframes glow {
0%, 100% { box-shadow: 0 -2px 10px rgba(218, 68, 83, 0.2); }
50% { box-shadow: 0 -2px 20px rgba(218, 68, 83, 0.4); }
}
.footer-stats {
display: flex;
gap: var(--spacing-md);
font-size: 0.75rem;
color: var(--text-secondary);
} }
</style> </style>