// Claude Desktop — Event-Bridge // Empfängt Events vom Tauri-Backend und aktualisiert die Stores import { listen, type UnlistenFn } from '@tauri-apps/api/event'; import { invoke } from '@tauri-apps/api/core'; import { get } from 'svelte/store'; import { agents, toolCalls, messages, isProcessing, addMessage, addAgent, addSubAgent, updateAgentStatus, addToolCall, completeToolCall, currentModel, sessionStats, contextUsage, currentSessionId, messageToDb, addMonitorEvent, loadMonitorEventsFromDb, activeKnowledgeHints, agentMode, type Message, type Agent, type MonitorEventType, type KnowledgeHint, type AgentMode } from './app'; // Event-Typen vom Backend interface AgentEvent { id: string; type?: string; task?: string; code?: number; model?: string; } interface SubagentEvent { id: string; parentAgentId: string; type?: string; task?: string; depth?: number; model?: string; toolUseId?: string; success?: boolean; } interface ToolEvent { id: string; tool?: string; input?: Record; output?: string; success?: boolean; } interface TextEvent { text: string; } interface ResultEvent { cost?: number; tokens?: { input: number; output: number; }; session_id?: string; model?: string; text?: string; } interface MonitorEventPayload { type: MonitorEventType; summary: string; details: Record; agentId?: string; durationMs?: number; error?: string; } // Listener-Handles let listeners: UnlistenFn[] = []; // Streaming: ID der aktuellen Live-Nachricht let streamingMessageId: string | null = null; // Nachricht in DB speichern async function saveMessageToDb(msg: Message) { const sessionId = get(currentSessionId); if (!sessionId) return; try { const dbMsg = messageToDb(msg, sessionId); await invoke('save_message', { message: dbMsg }); console.log('💾 Nachricht gespeichert:', msg.role); } catch (err) { console.error('Fehler beim Speichern der Nachricht:', err); } } // Events initialisieren export async function initEventListeners(): Promise { console.log('🎧 Initialisiere Event-Listener...'); await cleanupEventListeners(); // Monitor-Events aus DB laden (letzte Session) await loadMonitorEventsFromDb(500); // Bridge bereit listeners.push( await listen('bridge-ready', () => { console.log('✅ Bridge bereit'); }) ); // Session erstellt — Hook feuern + proaktive KB-Hints laden (fire-and-forget) listeners.push( await listen<{ id: string }>('session-created', (event) => { const { id } = event.payload; console.log('📂 Session-Created Event empfangen:', id); invoke('fire_hook', { event: 'SessionStart', summary: JSON.stringify({ sessionId: id }) }).catch((err) => console.debug('Hook session-start fehlgeschlagen:', err)); // Phase 2.0: Proaktive KB-Abfrage bei Session-Start loadProactiveSessionHints(); }) ); // Agent gestartet listeners.push( await listen('agent-started', (event) => { const { id, type, task, model } = event.payload; console.log('🤖 Agent gestartet:', id, type); // WICHTIG: id mitgeben, sonst stimmt parentAgentId der Sub-Agents nicht! addAgent(mapAgentType(type || 'main'), task || 'Verarbeite...', { id, model }); isProcessing.set(true); // Leere Streaming-Nachricht anlegen streamingMessageId = crypto.randomUUID(); messages.update((msgs) => [ ...msgs, { id: streamingMessageId!, role: 'assistant', content: '', timestamp: new Date(), agentId: id } ]); }) ); // Agent gestoppt listeners.push( await listen('agent-stopped', (event) => { const { id } = event.payload; console.log('⏹️ Agent gestoppt:', id); updateAgentStatus(id, 'stopped'); streamingMessageId = null; // Prüfen ob noch Agents aktiv agents.update((ags) => { const stillActive = ags.some((a) => a.status === 'active'); if (!stillActive) { isProcessing.set(false); } return ags; }); }) ); // Alle Agents gestoppt listeners.push( await listen('all-stopped', () => { console.log('⏹️ Alle Agents gestoppt'); agents.update((ags) => ags.map((a) => ({ ...a, status: 'stopped' as const }))); isProcessing.set(false); streamingMessageId = null; }) ); // Subagent gestartet listeners.push( await listen('subagent-started', (event) => { const { id, parentAgentId, type, task, depth, model } = event.payload; console.log('🤖 Subagent gestartet:', id, type, '(Parent:', parentAgentId, ')'); addSubAgent( parentAgentId, mapAgentType(type || 'explore'), task || 'Subagent-Aufgabe', { id, model } ); }) ); // Subagent gestoppt listeners.push( await listen('subagent-stopped', (event) => { const { id, success } = event.payload; console.log('⏹️ Subagent gestoppt:', id, success ? 'OK' : 'FEHLER'); updateAgentStatus(id, 'stopped'); }) ); // Tool Start listeners.push( await listen('tool-start', async (event) => { const { tool, input } = event.payload; console.log('🔧 Tool Start:', tool); agents.update((ags) => { const activeAgent = ags.find((a) => a.status === 'active'); if (activeAgent) { addToolCall(activeAgent.id, tool || 'unknown', input || {}); } return ags; }); // Hook: pre-tool-use (fire-and-forget, Fehler blockieren nicht) invoke('fire_hook', { event: 'PreToolUse', summary: JSON.stringify({ tool: tool || 'unknown', input: input || {} }) }).catch((err) => console.debug('Hook pre-tool-use fehlgeschlagen:', err)); // Wissens-Hints aus claude-db laden try { // Command aus Input extrahieren (je nach Tool) let command: string | undefined; if (input && typeof input === 'object') { // Bash: command, Read/Write/Edit: file_path command = (input as Record).command as string || (input as Record).file_path as string || undefined; } const hints = await invoke('get_tool_hints', { tool: tool || 'unknown', command, context: undefined }); if (hints && hints.length > 0) { activeKnowledgeHints.set(hints); console.log('💡 Wissens-Hints geladen:', hints.map(h => h.title)); } } catch (err) { // Fehler beim Laden ignorieren — Hints sind optional console.debug('Wissens-Hints nicht verfügbar:', err); } }) ); // Tool Ende listeners.push( await listen('tool-end', (event) => { const { id, tool, success, output } = event.payload; console.log('✅ Tool Ende:', id, success ? 'OK' : 'FEHLER'); completeToolCall(id, output, !success); // Hook: post-tool-use (fire-and-forget, Fehler blockieren nicht) invoke('fire_hook', { event: 'PostToolUse', summary: JSON.stringify({ tool: tool || 'unknown', success: !!success, hasOutput: !!output }) }).catch((err) => console.debug('Hook post-tool-use fehlgeschlagen:', err)); // Pattern-Detektion bei Tool-Fehlern (fire-and-forget) if (!success && output) { invoke<{ id: string; name: string; description: string; new_approach: string } | null>( 'detect_issue', { errorMessage: output, context: tool || 'unknown' } ).then((pattern) => { if (pattern) { console.log('🔍 Bekanntes Problem erkannt:', pattern.name); addMonitorEvent('error', `Bekanntes Problem: ${pattern.name}`, { patternId: pattern.id, beschreibung: pattern.description, loesung: pattern.new_approach, toolId: id, tool: tool || 'unknown', fehlerAuszug: output.substring(0, 200), }); } }).catch((err) => { // Pattern-Detektion ist optional — Fehler nur loggen console.debug('Pattern-Detektion fehlgeschlagen:', err); }); // Phase 2.0: Auto-Fehler-Tracking — Fehler hashen und zählen trackErrorOccurrence(output, tool || 'unknown'); } }) ); // Text-Streaming — live in die aktuelle Nachricht schreiben listeners.push( await listen('claude-text', (event) => { const { text } = event.payload; if (streamingMessageId) { messages.update((msgs) => msgs.map((m) => m.id === streamingMessageId ? { ...m, content: m.content + text } : m ) ); } }) ); // Ergebnis (Kosten, Token, Modell) listeners.push( await listen('claude-result', async (event) => { const { cost, tokens, session_id, model, text } = event.payload; console.log('📊 Ergebnis:', { cost: cost ? `$${cost.toFixed(4)}` : '-', tokens, model, session_id }); // Modell an die Streaming-Nachricht anhängen und speichern if (streamingMessageId) { let finalMessage: Message | null = null; messages.update((msgs) => { return msgs.map((m) => { if (m.id === streamingMessageId) { // Fallback: wenn kein Streaming-Text kam, result.text nutzen const content = m.content && m.content.trim() ? m.content : (text || ''); finalMessage = { ...m, content, model: model || m.model }; return finalMessage; } return m; }); }); // Nachricht in DB speichern (nur wenn Content vorhanden) if (finalMessage && finalMessage.content && finalMessage.content.trim()) { await saveMessageToDb(finalMessage); } } if (model) { currentModel.set(model); } // Claude Session-ID speichern für Fortsetzung if (session_id) { const appSessionId = get(currentSessionId); if (appSessionId) { try { await invoke('set_claude_session_id', { sessionId: appSessionId, claudeSessionId: session_id, }); console.log('🔗 Claude Session-ID gespeichert:', session_id); } catch (err) { console.warn('Claude Session-ID konnte nicht gespeichert werden:', err); } } } // 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, })); // Kontext-Auslastung aktualisieren (input_tokens = aktuelle Kontext-Größe) if (tokens?.input) { contextUsage.update((ctx) => ({ ...ctx, inputTokens: tokens.input, outputTokens: tokens.output || 0, })); } // Session-Stats in DB persistieren (überlebt App-Neustart) const appSessionId = get(currentSessionId); if (appSessionId) { const stats = get(sessionStats); invoke('update_session_stats', { sessionId: appSessionId, tokenInput: stats.totalTokensIn, tokenOutput: stats.totalTokensOut, costUsd: stats.totalCost, messageCount: stats.messageCount, }).catch((err: unknown) => console.warn('Session-Stats speichern fehlgeschlagen:', err)); } } }) ); // STOPP-Signal — nur Agents stoppen, Messages/Session bleiben erhalten listeners.push( await listen('agents-stopped', () => { console.log('🛑 STOPP-Signal empfangen'); streamingMessageId = null; // Alle Agents auf "stopped" setzen, aber Messages NICHT löschen agents.update((ags) => ags.map((a) => ({ ...a, status: 'stopped' as const }))); toolCalls.set([]); isProcessing.set(false); }) ); // Agent-Modus geändert (von Bridge bestätigt) listeners.push( await listen<{ mode: AgentMode }>('mode-changed', (event) => { const { mode } = event.payload; console.log('🔄 Agent-Modus geändert:', mode); agentMode.set(mode); }) ); // Monitor-Events — für System-Monitor Panel listeners.push( await listen('monitor', (event) => { const { type, summary, details, agentId, durationMs, error } = event.payload; addMonitorEvent(type, summary, details, { agentId, durationMs, error, }); }) ); console.log('✅ Event-Listener initialisiert'); } // Listener aufräumen export async function cleanupEventListeners(): Promise { for (const unlisten of listeners) { unlisten(); } listeners = []; } // Phase 2.0: Fehler-Hash berechnen (einfacher Hash aus Fehlermeldung) function hashError(errorMessage: string): string { // Normalisierung: Zahlen, Pfade und UUIDs entfernen für besseres Grouping const normalized = errorMessage .substring(0, 200) .replace(/\/[\w/.-]+/g, '') // Pfade .replace(/[0-9a-f]{8}-[0-9a-f]{4}/gi, '') // UUIDs .replace(/\d+/g, '') // Zahlen .replace(/\s+/g, ' ') // Whitespace .trim() .toLowerCase(); // Einfacher String-Hash let hash = 0; for (let i = 0; i < normalized.length; i++) { const char = normalized.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // 32-bit Integer } return 'err_' + Math.abs(hash).toString(36); } // Phase 2.0: Auto-Fehler-Tracking — Fehler zählen und bei 3+ automatisch Pattern in KB speichern async function trackErrorOccurrence(errorMessage: string, tool: string) { try { const errorHash = hashError(errorMessage); const [count, existingKbId] = await invoke<[number, number | null]>('track_error', { errorHash, errorMessage: errorMessage.substring(0, 1000), tool, }); console.log(`📊 Fehler-Tracking: ${errorHash} → ${count}x (KB: ${existingKbId || 'noch nicht'})`); // Bei 3+ Occurrences und noch kein KB-Eintrag: automatisch speichern if (count >= 3 && !existingKbId) { console.log(`🆕 Auto-Pattern: Fehler ${count}x aufgetreten, speichere in KB...`); const kbId = await invoke('auto_save_error_pattern', { errorHash, errorMessage: errorMessage.substring(0, 1000), tool, occurrenceCount: count, }); // KB-ID zurückschreiben await invoke('set_error_kb_pattern', { errorHash, kbPatternId: kbId }); addMonitorEvent('hook', `Auto-Pattern erstellt: KB #${kbId} (${count}x ${tool})`, { errorHash, kbId, occurrenceCount: count, tool, }); } } catch (err) { // Fehler-Tracking ist komplett optional — niemals die App blockieren console.debug('Fehler-Tracking fehlgeschlagen:', err); } } // Phase 2.0: Proaktive KB-Abfrage bei Session-Erstellung export async function loadProactiveSessionHints(projectName?: string): Promise { try { const hints = await invoke('get_session_hints', { projectName: projectName || null, }); if (hints && hints.length > 0) { console.log('📋 Proaktive Session-Hints geladen:', hints.length, 'Bytes'); addMonitorEvent('hook', `Proaktive KB-Hints geladen (~${Math.ceil(hints.length / 4)} Token)`, { projectName, hintSize: hints.length, }); } } catch (err) { console.debug('Proaktive Session-Hints nicht verfügbar:', err); } } // Agent-Typ mappen function mapAgentType(type: string): Agent['type'] { const typeMap: Record = { main: 'main', 'Main Agent': 'main', Main: 'main', explore: 'explore', Explore: 'explore', 'general-purpose': 'explore', plan: 'plan', Plan: 'plan', bash: 'bash', Bash: 'bash', code: 'code', Code: 'code', implement: 'code', test: 'test', Test: 'test', review: 'review', Review: 'review', }; return typeMap[type] || 'explore'; }