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:
parent
3cbf77e832
commit
e09fb8815c
3 changed files with 91 additions and 74 deletions
|
|
@ -50,6 +50,15 @@ export const permissions = writable<Permission[]>([]);
|
|||
export const isProcessing = writable(false);
|
||||
export const currentInput = writable('');
|
||||
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
|
||||
export const activeAgents = derived(agents, ($agents) =>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ import {
|
|||
updateAgentStatus,
|
||||
addToolCall,
|
||||
completeToolCall,
|
||||
clearAll
|
||||
clearAll,
|
||||
currentModel,
|
||||
sessionStats
|
||||
} from './app';
|
||||
|
||||
// Event-Typen vom Backend
|
||||
|
|
@ -168,6 +170,17 @@ export async function initEventListeners(): Promise<void> {
|
|||
messages.update((msgs) =>
|
||||
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,
|
||||
}));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@
|
|||
import '../app.css';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
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';
|
||||
|
||||
// Events beim Laden initialisieren
|
||||
onMount(async () => {
|
||||
await initEventListeners();
|
||||
});
|
||||
|
|
@ -14,9 +13,7 @@
|
|||
await cleanupEventListeners();
|
||||
});
|
||||
|
||||
// STOPP-Funktion
|
||||
async function handleStop() {
|
||||
console.log('STOPP gedrückt — breche alle Agents ab');
|
||||
try {
|
||||
await invoke('stop_all_agents');
|
||||
} catch (err) {
|
||||
|
|
@ -25,12 +22,23 @@
|
|||
$isProcessing = false;
|
||||
}
|
||||
|
||||
// Hotkey: Escape zum Stoppen
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && $isProcessing) {
|
||||
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>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
|
@ -39,38 +47,36 @@
|
|||
<!-- Titelleiste -->
|
||||
<header class="titlebar">
|
||||
<div class="titlebar-left">
|
||||
<span class="app-icon">🤖</span>
|
||||
<h1>Claude Desktop</h1>
|
||||
</div>
|
||||
<div class="titlebar-center">
|
||||
{#if $isProcessing}
|
||||
<span class="status-indicator active"></span>
|
||||
<span class="status-dot active"></span>
|
||||
<span>Arbeitet...</span>
|
||||
{:else}
|
||||
<span class="status-indicator idle"></span>
|
||||
<span class="status-dot idle"></span>
|
||||
<span>Bereit</span>
|
||||
{/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 class="titlebar-right">
|
||||
<span class="agent-stats">
|
||||
{$agentCount.active} aktiv | {$agentCount.waiting} wartend | {$agentCount.idle} idle
|
||||
</span>
|
||||
<span>{formatTokens($sessionStats.totalTokensIn)} in</span>
|
||||
<span class="sep">|</span>
|
||||
<span>{formatTokens($sessionStats.totalTokensOut)} out</span>
|
||||
<span class="sep">|</span>
|
||||
<span>{formatCost($sessionStats.totalCost)}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Haupt-Inhalt mit 3 Panels -->
|
||||
<main class="main-content">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<!-- STOPP-Button (immer sichtbar, unten) -->
|
||||
<!-- STOPP-Footer -->
|
||||
<footer class="stop-footer" class:active={$isProcessing}>
|
||||
<StopButton on:click={handleStop} disabled={!$isProcessing} />
|
||||
<div class="footer-stats">
|
||||
<span>Token: 0 / 200k</span>
|
||||
<span>|</span>
|
||||
<span>CPU: 0%</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
|
@ -82,93 +88,82 @@
|
|||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
/* Titelleiste */
|
||||
.titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
padding: var(--spacing-xs) var(--spacing-md);
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
-webkit-app-region: drag;
|
||||
border-bottom: 1px solid var(--border);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.titlebar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
font-size: 1.5rem;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.titlebar h1 {
|
||||
font-size: 1rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-heading);
|
||||
}
|
||||
|
||||
.titlebar-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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;
|
||||
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 {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* STOPP-Footer */
|
||||
.stop-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
padding: var(--spacing-xs) var(--spacing-md);
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid var(--bg-tertiary);
|
||||
transition: border-color 0.3s ease;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.stop-footer.active {
|
||||
border-top: 2px solid var(--accent);
|
||||
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);
|
||||
border-top: 2px solid var(--error);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue