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 @@
+
+
+
+
+
+
+
+
+ {#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}
+
+
+
+
+ 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'}