// Claude Desktop — App-State import { writable, derived } from 'svelte/store'; import { invoke } from '@tauri-apps/api/core'; // Typen export interface Agent { id: string; type: 'main' | 'explore' | 'plan' | 'bash' | 'code' | 'test' | 'review'; status: 'active' | 'waiting' | 'idle' | 'stopped'; task: string; startedAt: Date; toolCalls: ToolCall[]; // Subagent-Hierarchie parentAgentId?: string; // undefined = Main Agent depth: number; // 0 = Main, 1 = direkter Subagent, etc. model?: string; // Welches Modell nutzt dieser Agent } export interface ToolCall { id: string; agentId: string; tool: string; args: Record; status: 'running' | 'completed' | 'failed'; startedAt: Date; completedAt?: Date; result?: unknown; } // Inline Tool-Call der einer Message angehaengt ist (fuer Inline-Karten im Chat) export interface InlineToolCall { id: string; // toolId aus dem Backend tool: string; // Read, Edit, Write, Bash, Grep, Glob, WebFetch, Task, MCP, ... input: Record; status: 'running' | 'done' | 'error'; result?: string; // Stringifizierte Tool-Ausgabe (optional, wird beim tool-end gesetzt) startedAt: Date; completedAt?: Date; } // Phase 11: Chronologisch sortierte Stream-Teile einer Assistant-Message. // Loest die alte Trennung in `content`/`toolCalls`/`knowledgeHints` auf — // damit Tools jetzt zwischen Text-Stuecken erscheinen koennen, in der Reihenfolge // in der sie tatsaechlich passiert sind. export type MessagePart = | { type: 'text'; content: string } | { type: 'tool'; call: InlineToolCall }; export interface Message { id: string; role: 'user' | 'assistant' | 'system'; content: string; // aggregierter Text — fuer DB-Persistenz, FTS-Suche, Copy parts?: MessagePart[]; // chronologische Stream-Parts (Renderpfad) timestamp: Date; agentId?: string; model?: string; queued?: boolean; // Nachricht wartet in der Queue auf Dispatch toolCalls?: InlineToolCall[]; // Legacy: einige Stellen lesen noch hier — wird parallel gepflegt knowledgeHints?: KnowledgeHint[]; // KB-Treffer die fuer diese Antwort herangezogen wurden } // Hilfen fuer Stream-Handler in events.ts. // Text an die parts anhaengen: letzten text-part erweitern oder neuen anlegen. export function appendTextToParts(parts: MessagePart[] | undefined, text: string): MessagePart[] { const list = parts ? [...parts] : []; if (list.length > 0) { const last = list[list.length - 1]; if (last.type === 'text') { list[list.length - 1] = { type: 'text', content: last.content + text }; return list; } } list.push({ type: 'text', content: text }); return list; } // Tool-Part anfuegen (status='running'). export function appendToolToParts(parts: MessagePart[] | undefined, call: InlineToolCall): MessagePart[] { const list = parts ? [...parts] : []; list.push({ type: 'tool', call }); return list; } // Tool-Part finalisieren (status, result, completedAt setzen). export function updateToolInParts( parts: MessagePart[] | undefined, toolId: string, patch: Partial ): MessagePart[] | undefined { if (!parts) return parts; let changed = false; const next = parts.map((p) => { if (p.type === 'tool' && p.call.id === toolId) { changed = true; return { type: 'tool' as const, call: { ...p.call, ...patch } }; } return p; }); return changed ? next : parts; } // content-String aus parts aggregieren (fuer DB-Persistenz beim Result-Event). export function partsToContent(parts: MessagePart[] | undefined): string { if (!parts) return ''; return parts.filter((p) => p.type === 'text').map((p) => (p as { content: string }).content).join(''); } export interface Permission { id: string; pattern: string; type: 'session' | 'permanent'; action: 'allow' | 'deny'; createdAt: Date; } // Quick-Actions Typ (fuer QuickActions.svelte + ChatPanel.svelte) export interface QuickAction { id: string; label: string; description: string; icon: string; category: 'build' | 'git' | 'session' | 'navigation' | 'voice' | 'tools'; shortcut?: string; command?: string; // Wird als Message an Claude gesendet invoke?: string; // Tauri-Command direkt aufrufen invokeArgs?: Record; } // Pending File-Changes (Accept/Reject DiffView) export interface FileChange { toolId: string; tool: string; filePath: string; contentBefore: string; contentAfter: string; timestamp: Date; } export const pendingChanges = writable([]); // Stores export const agents = writable([]); export const toolCalls = writable([]); export const messages = writable([]); export const permissions = writable([]); // Phase 11: Approval-Modus. // - 'default' = Approval-Bar fragt bei jedem Edit/Bash // - 'acceptEdits' = Datei-Edits laufen automatisch durch, Bash bleibt Approval-pflichtig // - 'bypassPermissions' = alles automatisch (Yolo) — Guard-Rails greifen nicht mehr export type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions'; export const permissionMode = writable('default'); // UI-State export const isProcessing = writable(false); export const chatDetached = writable(false); export const currentInput = writable(''); export const selectedAgentId = writable(null); export const currentModel = writable(''); export const currentSessionId = writable(null); // Message-Queue: Nachrichten die waehrend der Verarbeitung eingehen, werden hier // gesammelt und nach Ende der aktuellen Antwort FIFO abgearbeitet. // Jede Nachricht erscheint sofort im Chat als User-Message. export const messageQueue = writable([]); // Abwaertskompatibel: queuedMessage zeigt die naechste wartende Nachricht // (Legacy — wird in ChatPanel noch referenziert) export const queuedMessage = writable(null); // Agent-Modus für Multi-Agent-Architektur export type AgentMode = 'solo' | 'handlanger' | 'experten' | 'auto'; export const agentMode = writable('solo'); // Session-Statistiken (kumuliert) export const sessionStats = writable({ totalTokensIn: 0, totalTokensOut: 0, totalCost: 0, messageCount: 0, }); // Kontext-Auslastung (aktueller API-Call) // inputTokens = was Claude bei diesem Request "gelesen" hat (System + Konversation) export const contextUsage = writable({ inputTokens: 0, // Aktuelle Kontext-Tokens outputTokens: 0, // Tokens der letzten Antwort contextLimit: 200000, // Claude 3.5/Opus Context Window }); // Abgeleitet: Prozent der Kontext-Auslastung export const contextPercent = derived(contextUsage, ($ctx) => Math.round(($ctx.inputTokens / $ctx.contextLimit) * 100) ); // Sticky Context Status (beim App-Start geladen) export interface StickyContextInfo { loaded: boolean; entries: number; estimatedTokens: number; hasUserInfo: boolean; hasProject: boolean; credentialsCount: number; rulesCount: number; } export const stickyContextInfo = writable(null); // Wissens-Hints (aus claude-db) export interface KnowledgeHint { id: number; category: string; title: string; content: string; tags?: string; priority: number; } export const activeKnowledgeHints = writable([]); // Abgeleitete Stores export const activeAgents = derived(agents, ($agents) => $agents.filter((a) => a.status === 'active') ); export const recentToolCalls = derived(toolCalls, ($toolCalls) => $toolCalls.slice(-50).reverse() ); export const agentCount = derived(agents, ($agents) => ({ total: $agents.length, active: $agents.filter((a) => a.status === 'active').length, waiting: $agents.filter((a) => a.status === 'waiting').length, idle: $agents.filter((a) => a.status === 'idle').length, mainAgents: $agents.filter((a) => !a.parentAgentId).length, subAgents: $agents.filter((a) => a.parentAgentId).length, })); // Agent-Baum Typen und Builder (muss vor agentTree Store sein) export interface AgentTreeNode { agent: Agent; children: AgentTreeNode[]; } export function buildAgentTree(agentsList: Agent[]): AgentTreeNode[] { // Nur Root-Agents (ohne Parent) const roots = agentsList.filter((a) => !a.parentAgentId); function buildNode(agent: Agent): AgentTreeNode { const children = agentsList .filter((a) => a.parentAgentId === agent.id) .map(buildNode); return { agent, children }; } return roots.map(buildNode); } // Agent-Baum als reaktiver Store export const agentTree = derived(agents, ($agents) => buildAgentTree($agents)); // Aktionen export function addMessage(role: Message['role'], content: string, agentId?: string) { messages.update((msgs) => [ ...msgs, { id: crypto.randomUUID(), role, content, parts: content ? [{ type: 'text', content }] : [], timestamp: new Date(), agentId } ]); } export interface AddAgentOptions { id?: string; parentAgentId?: string; model?: string; } export function addAgent(type: Agent['type'], task: string, options?: AddAgentOptions): string { const id = options?.id || crypto.randomUUID(); const parentAgentId = options?.parentAgentId; // Tiefe berechnen: Parent-Tiefe + 1 (oder 0 wenn kein Parent) let depth = 0; if (parentAgentId) { agents.subscribe((ags) => { const parent = ags.find((a) => a.id === parentAgentId); if (parent) { depth = parent.depth + 1; } })(); } agents.update((ags) => [ ...ags, { id, type, status: 'active', task, startedAt: new Date(), toolCalls: [], parentAgentId, depth, model: options?.model, } ]); return id; } // Subagent hinzufügen (Kurzform) export function addSubAgent( parentId: string, type: Agent['type'], task: string, options?: Omit ): string { return addAgent(type, task, { ...options, parentAgentId: parentId }); } // Alle Kinder eines Agents finden export function getChildAgents(parentId: string, agentsList: Agent[]): Agent[] { return agentsList.filter((a) => a.parentAgentId === parentId); } export function updateAgentStatus(id: string, status: Agent['status']) { agents.update((ags) => ags.map((a) => (a.id === id ? { ...a, status } : a)) ); } export function addToolCall(agentId: string, tool: string, args: Record, fixedId?: string): string { const id = fixedId || crypto.randomUUID(); const call: ToolCall = { id, agentId, tool, args, status: 'running', startedAt: new Date() }; toolCalls.update((calls) => [...calls, call]); // Auch im Agent speichern agents.update((ags) => ags.map((a) => a.id === agentId ? { ...a, toolCalls: [...a.toolCalls, call] } : a ) ); return id; } export function completeToolCall(id: string, result: unknown, failed = false) { toolCalls.update((calls) => { // Exakte ID-Suche let found = calls.some((c) => c.id === id); if (found) { return calls.map((c) => c.id === id ? { ...c, status: (failed ? 'failed' : 'completed') as ToolCall['status'], completedAt: new Date(), result } : c ); } // Fallback: Letzten laufenden Tool-Call abschließen (für Events ohne passende ID) const lastRunning = [...calls].reverse().find((c) => c.status === 'running'); if (lastRunning) { return calls.map((c) => c.id === lastRunning.id ? { ...c, status: (failed ? 'failed' : 'completed') as ToolCall['status'], completedAt: new Date(), result } : c ); } return calls; }); } export function clearAll() { agents.set([]); toolCalls.set([]); messages.set([]); isProcessing.set(false); } // DB-Nachricht Format (für Tauri) export interface DbMessage { id: string; session_id: string; role: string; content: string; model: string | null; agent_id: string | null; // Agent der die Nachricht erzeugt hat timestamp: string; } // Konvertierung: Store → DB export function messageToDb(msg: Message, sessionId: string): DbMessage { return { id: msg.id, session_id: sessionId, role: msg.role, content: msg.content, model: msg.model || null, agent_id: msg.agentId || null, timestamp: msg.timestamp.toISOString(), }; } // Konvertierung: DB → Store export function dbToMessage(db: DbMessage): Message { const role = db.role as Message['role']; // Beim Reload aus der DB sind nur text-Parts wiederherstellbar — Tool-Calls // werden nicht persistiert (waren sie vorher auch nicht). Der Renderer // laeuft trotzdem ueber parts, damit die Logik in beiden Faellen gleich ist. const parts: MessagePart[] | undefined = role === 'assistant' && db.content ? [{ type: 'text', content: db.content }] : undefined; return { id: db.id, role, content: db.content, parts, model: db.model || undefined, agentId: db.agent_id || undefined, timestamp: new Date(db.timestamp), }; } // Nachrichten aus DB in Store laden export function setMessagesFromDb(dbMessages: DbMessage[]) { messages.set(dbMessages.map(dbToMessage)); } // ============ System-Monitor ============ export type MonitorEventType = 'api' | 'hook' | 'tool' | 'mcp' | 'agent' | 'error' | 'debug'; export interface MonitorEvent { id: string; timestamp: Date; type: MonitorEventType; summary: string; // Einzeiler für Kompakt-Ansicht details: Record; // Vollständige Daten sessionId?: string; agentId?: string; durationMs?: number; error?: string; } // Farbcodierung für Event-Typen export const monitorEventColors: Record = { api: '🔵', hook: '🟢', tool: '🟡', mcp: '🟣', agent: '🟠', error: '🔴', debug: '⚪', }; // Monitor Store — Ringbuffer mit max 1000 Events const MAX_MONITOR_EVENTS = 1000; export const monitorEvents = writable([]); // Filter für Monitor-Ansicht export const monitorFilter = writable('all'); export const monitorAutoScroll = writable(true); export const selectedMonitorEventId = writable(null); // Gefilterte Events export const filteredMonitorEvents = derived( [monitorEvents, monitorFilter], ([$events, $filter]) => { if ($filter === 'all') return $events; return $events.filter((e) => e.type === $filter); } ); // Monitor-Statistiken export const monitorStats = derived(monitorEvents, ($events) => { const last100 = $events.slice(-100); const apiEvents = last100.filter((e) => e.type === 'api'); const errorEvents = last100.filter((e) => e.type === 'error'); const avgLatency = apiEvents.length > 0 ? apiEvents.reduce((sum, e) => sum + (e.durationMs || 0), 0) / apiEvents.length : 0; return { totalEvents: $events.length, apiCalls: apiEvents.length, errors: errorEvents.length, avgLatencyMs: Math.round(avgLatency), }; }); // Monitor-Event hinzufügen (mit Persistierung) export function addMonitorEvent( type: MonitorEventType, summary: string, details: Record = {}, options?: Partial> ) { const event: MonitorEvent = { id: crypto.randomUUID(), timestamp: new Date(), type, summary, details, ...options, }; monitorEvents.update((events) => { const updated = [...events, event]; // Ringbuffer: Alte Events entfernen wenn zu viele if (updated.length > MAX_MONITOR_EVENTS) { return updated.slice(-MAX_MONITOR_EVENTS); } return updated; }); // Asynchron in DB speichern (fire-and-forget) saveMonitorEventToDb(event).catch((err) => { console.warn('Monitor-Event konnte nicht gespeichert werden:', err); }); return event.id; } // Monitor-Event in DB speichern async function saveMonitorEventToDb(event: MonitorEvent): Promise { // Für DB-Speicherung: Timestamp als ISO-String, Details als JSON-String const dbEvent = { id: event.id, timestamp: event.timestamp.toISOString(), event_type: event.type, summary: event.summary, details: JSON.stringify(event.details), agent_id: event.agentId ?? null, session_id: null, // TODO: Aktuelle Session-ID übergeben duration_ms: event.durationMs ?? null, error: event.error ?? null, }; await invoke('save_monitor_event', { event: dbEvent }); } // Monitor-Events aus DB laden export async function loadMonitorEventsFromDb(limit = 500): Promise { try { interface DbMonitorEvent { id: string; timestamp: string; event_type: string; summary: string; details: string | null; agent_id: string | null; session_id: string | null; duration_ms: number | null; error: string | null; } const dbEvents = await invoke('load_monitor_events', { limit }); // DB-Events in Frontend-Format umwandeln (neueste zuerst → umkehren für chronologische Reihenfolge) const events: MonitorEvent[] = dbEvents.reverse().map((e) => ({ id: e.id, timestamp: new Date(e.timestamp), type: e.event_type as MonitorEventType, summary: e.summary, details: e.details ? JSON.parse(e.details) : {}, agentId: e.agent_id ?? undefined, durationMs: e.duration_ms ?? undefined, error: e.error ?? undefined, })); monitorEvents.set(events); console.log(`📊 ${events.length} Monitor-Events aus DB geladen`); } catch (err) { console.error('Fehler beim Laden der Monitor-Events:', err); } } // Monitor leeren (auch in DB) export async function clearMonitorEvents(): Promise { monitorEvents.set([]); selectedMonitorEventId.set(null); try { const count = await invoke('clear_all_monitor_events'); console.log(`🗑️ ${count} Monitor-Events aus DB gelöscht`); } catch (err) { console.warn('Monitor-Events konnten nicht aus DB gelöscht werden:', err); } } // Sensitive Daten maskieren export function maskSensitive(data: string): string { return data .replace(/password[=:]\s*\S+/gi, 'password=***') .replace(/api[_-]?key[=:]\s*\S+/gi, 'api_key=***') .replace(/bearer\s+\S+/gi, 'Bearer ***') .replace(/sk-[a-zA-Z0-9]+/g, 'sk-***') .replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '***@***.***'); }