diff --git a/scripts/claude-bridge.js b/scripts/claude-bridge.js index 9c1b8e4..13fb0ec 100644 --- a/scripts/claude-bridge.js +++ b/scripts/claude-bridge.js @@ -68,6 +68,52 @@ function sendError(id, error) { sendToTauri({ type: 'response', id, error }); } +// ============ Monitor-Events ============ + +// Sendet ein Event für den System-Monitor +function sendMonitorEvent(type, summary, details = {}, options = {}) { + sendEvent('monitor', { + type, // 'api' | 'hook' | 'tool' | 'mcp' | 'agent' | 'error' | 'debug' + summary, // Einzeiler für Kompakt-Ansicht + details, // Vollständige Daten + agentId: options.agentId || currentAgentId, + durationMs: options.durationMs, + error: options.error, + }); +} + +// Tool-Input für Logging kürzen (sensitive Daten maskieren) +function summarizeToolInput(tool, input) { + if (!input) return ''; + + // Bestimmte Tools speziell behandeln + if (tool === 'Read') { + return input.file_path || ''; + } + if (tool === 'Edit' || tool === 'Write') { + const path = input.file_path || ''; + const size = input.content ? `(${input.content.length} chars)` : ''; + return `${path} ${size}`; + } + if (tool === 'Grep') { + return `"${input.pattern}" in ${input.path || '.'}`; + } + if (tool === 'Bash') { + const cmd = input.command || ''; + return cmd.length > 50 ? cmd.substring(0, 50) + '...' : cmd; + } + if (tool === 'Task') { + return input.description || input.prompt || ''; + } + + // Default: Erstes String-Feld nehmen + const firstString = Object.values(input).find(v => typeof v === 'string'); + if (firstString) { + return firstString.length > 50 ? firstString.substring(0, 50) + '...' : firstString; + } + return ''; +} + // ============ Claude Agent SDK ============ async function sendMessage(message, requestId, model = null) { @@ -84,6 +130,20 @@ async function sendMessage(message, requestId, model = null) { model: useModel, }); + // Monitor: Agent gestartet + sendMonitorEvent('agent', `Main Agent gestartet (${useModel})`, { + agentId: currentAgentId, + model: useModel, + task: message.substring(0, 100), + }); + + // Monitor: API-Request + sendMonitorEvent('api', `→ ${useModel}`, { + model: useModel, + promptLength: message.length, + maxTurns: 25, + }); + sendResponse(requestId, { agentId: currentAgentId, status: 'gestartet', model: useModel }); const startTime = Date.now(); @@ -159,6 +219,14 @@ async function sendMessage(message, requestId, model = null) { input: toolInput, agentId: currentAgentId, }); + + // Monitor: Tool gestartet + const toolSummary = summarizeToolInput(toolName, toolInput); + sendMonitorEvent('tool', `${toolName} ${toolSummary}`, { + toolId, + tool: toolName, + input: toolInput, + }); break; } @@ -185,20 +253,33 @@ async function sendMessage(message, requestId, model = null) { break; } - case 'result': + case 'result': { // Endergebnis + const durationMs = Date.now() - startTime; + const inputTokens = event.usage?.input_tokens || 0; + const outputTokens = event.usage?.output_tokens || 0; + const cost = event.total_cost_usd || 0; + sendEvent('result', { text: fullText, - cost: event.total_cost_usd || 0, - tokens: { - input: event.usage?.input_tokens || 0, - output: event.usage?.output_tokens || 0, - }, + cost, + tokens: { input: inputTokens, output: outputTokens }, session_id: event.session_id || '', - duration_ms: Date.now() - startTime, + duration_ms: durationMs, model: usedModel, }); + + // Monitor: API-Response + const tokenK = ((inputTokens + outputTokens) / 1000).toFixed(1); + sendMonitorEvent('api', `← ${usedModel} [${durationMs}ms] ${tokenK}k tok $${cost.toFixed(4)}`, { + model: usedModel, + inputTokens, + outputTokens, + cost, + sessionId: event.session_id, + }, { durationMs }); break; + } default: // Andere Events still ignorieren @@ -208,8 +289,16 @@ async function sendMessage(message, requestId, model = null) { } catch (err) { if (err.name === 'AbortError') { // Abgebrochen — kein Fehler + sendMonitorEvent('agent', 'Abgebrochen (User)', { reason: 'abort' }); } else { sendEvent('text', { text: `\n\n**Fehler:** ${err.message || err}` }); + + // Monitor: Fehler + sendMonitorEvent('error', `${err.message || err}`, { + name: err.name, + message: err.message, + stack: err.stack, + }, { error: err.message || String(err) }); } } finally { // Alle noch aktiven Subagents stoppen diff --git a/src/lib/components/MonitorPanel.svelte b/src/lib/components/MonitorPanel.svelte new file mode 100644 index 0000000..8d20cc9 --- /dev/null +++ b/src/lib/components/MonitorPanel.svelte @@ -0,0 +1,438 @@ + + +
+ +
+
+

📊 System-Monitor

+
+ {$monitorStats.totalEvents} + {$monitorStats.apiCalls} API + {#if $monitorStats.errors > 0} + {$monitorStats.errors} Err + {/if} + {#if $monitorStats.avgLatencyMs > 0} + {$monitorStats.avgLatencyMs}ms + {/if} +
+
+ +
+ + + + + +
+
+ + +
+ {#if $filteredMonitorEvents.length === 0} +
+

Keine Events.

+

Events erscheinen hier wenn Claude arbeitet.

+
+ {:else} + {#each $filteredMonitorEvents as event (event.id)} + + {/each} + {/if} +
+ + + {#if $selectedMonitorEventId} + {@const selectedEvent = $filteredMonitorEvents.find((e) => e.id === $selectedMonitorEventId)} + {#if selectedEvent} +
+
+

+ {monitorEventColors[selectedEvent.type]} {typeLabels[selectedEvent.type]} +

+ +
+ +
+ Zeit: + {selectedEvent.timestamp.toLocaleString('de-DE')} +
+
+ Summary: + {selectedEvent.summary} +
+ {#if selectedEvent.durationMs} +
+ Dauer: + {selectedEvent.durationMs}ms +
+ {/if} + {#if selectedEvent.agentId} +
+ Agent: + {selectedEvent.agentId.substring(0, 8)}... +
+ {/if} + {#if selectedEvent.error} +
+ Fehler: + {selectedEvent.error} +
+ {/if} + +

Details

+
{formatDetails(selectedEvent.details)}
+
+ {/if} + {/if} +
+ + diff --git a/src/lib/stores/app.ts b/src/lib/stores/app.ts index 23a5778..eddbb7e 100644 --- a/src/lib/stores/app.ts +++ b/src/lib/stores/app.ts @@ -261,3 +261,108 @@ export function dbToMessage(db: DbMessage): Message { 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 +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; + }); + + return event.id; +} + +// Monitor leeren +export function clearMonitorEvents() { + monitorEvents.set([]); +} + +// 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, '***@***.***'); +} diff --git a/src/lib/stores/events.ts b/src/lib/stores/events.ts index b2e3d56..a9b4c03 100644 --- a/src/lib/stores/events.ts +++ b/src/lib/stores/events.ts @@ -20,8 +20,10 @@ import { sessionStats, currentSessionId, messageToDb, + addMonitorEvent, type Message, - type Agent + type Agent, + type MonitorEventType } from './app'; // Event-Typen vom Backend @@ -66,6 +68,15 @@ interface ResultEvent { model?: string; } +interface MonitorEventPayload { + type: MonitorEventType; + summary: string; + details: Record; + agentId?: string; + durationMs?: number; + error?: string; +} + // Listener-Handles let listeners: UnlistenFn[] = []; @@ -267,6 +278,19 @@ export async function initEventListeners(): Promise { }) ); + // 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'); } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index bcf2c52..6e4f250 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -8,12 +8,14 @@ import AuditLog from '$lib/components/AuditLog.svelte'; import GuardRailsPanel from '$lib/components/GuardRailsPanel.svelte'; import SettingsPanel from '$lib/components/SettingsPanel.svelte'; + import MonitorPanel from '$lib/components/MonitorPanel.svelte'; let activeMiddleTab = 'activity'; let activeRightTab = 'agents'; const middleTabs = [ { id: 'activity', label: 'Aktivität', icon: '📋' }, + { id: 'monitor', label: 'Monitor', icon: '📊' }, { id: 'memory', label: 'Gedächtnis', icon: '🧠' }, { id: 'audit', label: 'Historie', icon: '📝' }, ]; @@ -60,6 +62,8 @@
{#if activeMiddleTab === 'activity'} + {:else if activeMiddleTab === 'monitor'} + {:else if activeMiddleTab === 'memory'} {:else if activeMiddleTab === 'audit'}