Some checks failed
Build AppImage / build (push) Has been cancelled
- Long messages (>25 lines) auto-collapse with expand/collapse button - Cost display uses German format: "16,23$" instead of "$16.230" - Session stats (tokens, cost, count) persist to DB after each response via new update_session_stats command — survives app restart - Small costs shown as cents (e.g. "3,2¢") Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
548 lines
15 KiB
TypeScript
548 lines
15 KiB
TypeScript
// 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<string, unknown>;
|
|
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<string, unknown>;
|
|
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<void> {
|
|
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<AgentEvent>('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<AgentEvent>('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<SubagentEvent>('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<SubagentEvent>('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<ToolEvent>('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<string, unknown>).command as string
|
|
|| (input as Record<string, unknown>).file_path as string
|
|
|| undefined;
|
|
}
|
|
|
|
const hints = await invoke<KnowledgeHint[]>('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<ToolEvent>('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<TextEvent>('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<ResultEvent>('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<MonitorEventPayload>('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<void> {
|
|
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, '<PATH>') // Pfade
|
|
.replace(/[0-9a-f]{8}-[0-9a-f]{4}/gi, '<UUID>') // UUIDs
|
|
.replace(/\d+/g, '<N>') // 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<number>('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<void> {
|
|
try {
|
|
const hints = await invoke<string>('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<string, Agent['type']> = {
|
|
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';
|
|
}
|