claude-desktop/src/routes/+layout.svelte
Eddy 79b8525ede Bugfixes: Resume, Tool-Whitelist, Sub-Agent-Tree, UI-Polish
Bridge (claude-bridge.js):
- Resume-Fix: queryOptions.resume statt .sessionId (SDK-API)
- tools-Whitelist statt disallowedTools (Blacklist vererbt sich auf Sub-Agents!)
  Handlanger: Main nur Task+TodoWrite, Sub-Agents bekommen volles Tool-Set
  Experten: Main nur Task+TodoWrite+Read+Grep+Glob
  Solo: preset claude_code
- handleToolUse/handleToolResult Helper, greifen auch in assistant.content-Bloecken
  (SDK liefert tool_use/tool_result nicht als standalone events)
- Dedup via handledTools Set
- Resume-Retry-Fallback bei ungueltiger Session-ID
- Custom agents-Option entfernt (SDK spawnt Sub-Agents ohne Tools → Halluzination)
- Orchestrator-Prompt: verweist auf general-purpose (vollstaendiges Tool-Set)

Backend (claude.rs):
- claude_session_id NUR beim 1. Mal setzen (sonst verliert man History)
- Generic event emit fuer alle Bridge-Events ans Frontend
- Mode-Persistenz bei Bridge-Start (agent_mode aus DB laden)

Knowledge (knowledge.rs):
- MYSQL_HOST: 192.168.155.1 → 192.168.155.11 (MariaDB-Server)
- MYSQL_PASS: claude → 8715
- category Option<&str> Typ-Annotation fuer exec_map

Programs (programs.rs):
- xvfb_screenshot: Fallback scrot → import (ImageMagick) → ffmpeg

Voice (voice.rs):
- Part::file (existiert nicht) → Part::bytes, keine Temp-Datei

Frontend:
- events.ts: mode-changed Listener, result.text Fallback,
  addAgent({id}) fuer korrekte Parent-Child-Verknuepfung
- ChatPanel: Copy-Button, Typing-Dots in Bubble (kein Doppel-Header),
  $effect statt $:, onkeydown statt on:keydown
- AgentView: "Nur aktive" Toggle, Delegations-Badge, Tool-Count hidden bei 0,
  agentMode Import
- ProgramsPanel: Button-Styling, Error-Banner mit Copy-Button,
  selectable Text
- MonitorPanel: Filter-Dropdown Styling (Hintergrund + Hover)
- SettingsPanel: changeMode() wird beim Klick aufgerufen (nicht nur Store)
- +layout.svelte: agent_mode beim App-Start laden, Mode-Badge im Footer,
  🎓-Button fuer Schulungsfenster
- +page.svelte: Programme-Tab + Hooks-Tab

Neue Dateien:
- TEST-ROADMAP.md — Status und naechste Schritte
- .gitignore erweitert (scheduled_tasks.lock, out/, node_modules)
- vscode-extension/tsconfig.json: include nur src/, exclude node_modules

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:24:51 +02:00

350 lines
8.5 KiB
Svelte

<script lang="ts">
import '../app.css';
import { onMount, onDestroy } from 'svelte';
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 StopButton from '$lib/components/StopButton.svelte';
// Session-Typ vom Backend
interface Session {
id: string;
claude_session_id: string | null;
title: string;
working_dir: string | null;
message_count: number;
token_input: number;
token_output: number;
cost_usd: number;
status: string;
created_at: string;
updated_at: string;
last_message: string | null;
}
// Backend-Response für Sticky Context
interface StickyContextResponse {
loaded: boolean;
entries: number;
estimated_tokens: number;
has_user_info: boolean;
has_project: boolean;
credentials_count: number;
rules_count: number;
}
onMount(async () => {
await initEventListeners();
// Aktuelles Modell aus Settings laden
try {
const model: string = await invoke('get_current_model');
if (model) {
$currentModel = model;
}
} catch (err) {
console.warn('Modell konnte nicht geladen werden:', err);
}
// Agent-Modus aus Settings laden (sonst Badge nicht sofort sichtbar)
try {
const mode: string = await invoke('get_agent_mode');
if (mode) {
$agentMode = mode as AgentMode;
}
} catch (err) {
console.warn('Agent-Modus konnte nicht geladen werden:', err);
}
// Sticky Context beim Start laden und an Bridge senden
try {
const ctx: StickyContextResponse = await invoke('init_sticky_context');
$stickyContextInfo = {
loaded: ctx.loaded,
entries: ctx.entries,
estimatedTokens: ctx.estimated_tokens,
hasUserInfo: ctx.has_user_info,
hasProject: ctx.has_project,
credentialsCount: ctx.credentials_count,
rulesCount: ctx.rules_count,
};
if (ctx.loaded) {
console.log(`📌 Sticky Context geladen: ${ctx.entries} Einträge, ~${ctx.estimated_tokens} Token`);
}
} catch (err) {
console.warn('Sticky Context konnte nicht geladen werden:', err);
}
// Aktive Session automatisch laden (falls vorhanden)
try {
const activeSession: Session | null = await invoke('get_active_session');
if (activeSession) {
console.log('📂 Lade aktive Session:', activeSession.title);
$currentSessionId = activeSession.id;
// Session-Stats setzen
$sessionStats = {
totalTokensIn: activeSession.token_input,
totalTokensOut: activeSession.token_output,
totalCost: activeSession.cost_usd,
messageCount: activeSession.message_count,
};
// Nachrichten laden
const messages: DbMessage[] = await invoke('load_messages', { sessionId: activeSession.id });
if (messages.length > 0) {
setMessagesFromDb(messages);
console.log(`💬 ${messages.length} Nachrichten geladen`);
}
}
} catch (err) {
console.warn('Aktive Session konnte nicht geladen werden:', err);
}
});
onDestroy(async () => {
await cleanupEventListeners();
});
async function handleStop() {
try {
await invoke('stop_all_agents');
} catch (err) {
console.error('Fehler beim Stoppen:', err);
}
$isProcessing = false;
}
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} />
<div class="app-container">
<!-- Titelleiste -->
<header class="titlebar">
<div class="titlebar-left">
<h1>Claude Desktop</h1>
</div>
<div class="titlebar-center">
{#if $isProcessing}
<span class="status-dot active"></span>
<span>Arbeitet...</span>
{:else}
<span class="status-dot idle"></span>
<span>Bereit</span>
{/if}
</div>
<div class="titlebar-right">
<button
class="teach-btn"
title="Schulungsmodus (Präsentations-Fenster)"
onclick={() => invoke('presentation_open').catch(e => console.error(e))}
>
🎓
</button>
{#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>
</header>
<main class="main-content">
<slot />
</main>
<!-- Footer: STOPP + Stats -->
<footer class="footer" class:active={$isProcessing}>
<StopButton on:click={handleStop} disabled={!$isProcessing} />
<div class="footer-stats">
{#if $stickyContextInfo?.loaded}
<span class="context-badge" title="Sticky Context: {$stickyContextInfo.entries} Einträge, ~{$stickyContextInfo.estimatedTokens} Token">
📌 +{$stickyContextInfo.estimatedTokens}ctx
</span>
<span class="sep">|</span>
{/if}
<span>Token: {formatTokens($sessionStats.totalTokensIn)} in / {formatTokens($sessionStats.totalTokensOut)} out</span>
<span class="sep">|</span>
<span>Kosten: {formatCost($sessionStats.totalCost)}</span>
<span class="sep">|</span>
<span>{$sessionStats.messageCount} Antworten</span>
{#if $currentModel}
<span class="sep">|</span>
<span class="model">{$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}
{#if $agentMode && $agentMode !== 'solo'}
<span class="sep">|</span>
<span class="mode-badge mode-{$agentMode}" title="Agent-Modus: {$agentMode}">
{#if $agentMode === 'handlanger'}👷 Handlanger
{:else if $agentMode === 'experten'}🎓 Experten
{:else if $agentMode === 'auto'}🤖 Auto
{/if}
</span>
{/if}
</div>
</footer>
</div>
<style>
.app-container {
display: flex;
flex-direction: column;
height: 100vh;
background: var(--bg-primary);
}
.titlebar {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-xs) var(--spacing-md);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
user-select: none;
height: 36px;
}
.titlebar h1 {
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.75rem;
color: var(--text-secondary);
}
.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;
}
.footer {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-md);
background: var(--bg-secondary);
border-top: 1px solid var(--border);
}
.footer.active {
border-top: 2px solid var(--error);
}
.footer-stats {
display: flex;
gap: var(--spacing-sm);
font-size: 0.65rem;
color: var(--text-secondary);
font-family: var(--font-mono);
}
.footer-stats .sep {
opacity: 0.3;
}
.footer-stats .model {
color: var(--accent);
font-weight: 600;
}
.footer-stats .context-badge {
color: #22c55e;
font-weight: 500;
cursor: help;
}
.footer-stats .mode-badge {
font-weight: 600;
cursor: help;
padding: 1px 6px;
border-radius: 3px;
}
.footer-stats .mode-handlanger {
color: #f59e0b;
background: rgba(245, 158, 11, 0.12);
}
.footer-stats .mode-experten {
color: #a855f7;
background: rgba(168, 85, 247, 0.12);
}
.footer-stats .mode-auto {
color: #06b6d4;
background: rgba(6, 182, 212, 0.12);
}
.teach-btn {
background: transparent;
border: 1px solid var(--border);
color: var(--text-primary);
font-size: 0.9rem;
padding: 2px 8px;
border-radius: 4px;
cursor: pointer;
margin-right: var(--spacing-xs);
}
.teach-btn:hover {
background: rgba(96, 165, 250, 0.15);
}
</style>