Compare commits

..

No commits in common. "9d837efae69093c93781e38a354118dc58493682" and "4ba14a53e19830eb1b4b9df90c87b4e4a9dddd4d" have entirely different histories.

12 changed files with 64 additions and 2913 deletions

1300
ROADMAP.md

File diff suppressed because it is too large Load diff

View file

@ -19,10 +19,6 @@ let activeAbort = null;
let currentAgentId = null;
let currentModel = process.env.CLAUDE_MODEL || 'opus';
// Subagent-Tracking
// Map: toolUseId → { agentId, parentId, type, task, depth }
const activeSubagents = new Map();
// Verfügbare Modelle
const AVAILABLE_MODELS = [
{ id: 'haiku', name: 'Claude Haiku', description: 'Schnell & günstig' },
@ -30,26 +26,6 @@ const AVAILABLE_MODELS = [
{ id: 'opus', name: 'Claude Opus', description: 'Leistungsstark' },
];
// Tools die Subagents spawnen
const SUBAGENT_TOOLS = ['Task', 'Agent', 'spawn_agent', 'launch_agent'];
// Subagent-Typ aus Tool-Input ermitteln
function getSubagentType(toolName, input) {
if (input?.subagent_type) return input.subagent_type.toLowerCase();
if (input?.agent_type) return input.agent_type.toLowerCase();
// Fallback basierend auf description/prompt
const desc = (input?.description || input?.prompt || '').toLowerCase();
if (desc.includes('explore') || desc.includes('search') || desc.includes('find')) return 'explore';
if (desc.includes('plan') || desc.includes('design')) return 'plan';
if (desc.includes('bash') || desc.includes('command') || desc.includes('terminal')) return 'bash';
if (desc.includes('code') || desc.includes('implement') || desc.includes('write')) return 'code';
if (desc.includes('test') || desc.includes('verify')) return 'test';
if (desc.includes('review') || desc.includes('check')) return 'review';
return 'explore'; // Default
}
// ============ Kommunikation mit Tauri ============
function sendToTauri(msg) {
@ -68,52 +44,6 @@ 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) {
@ -130,20 +60,6 @@ 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();
@ -177,109 +93,35 @@ async function sendMessage(message, requestId, model = null) {
}
break;
case 'tool_use': {
const toolId = event.tool_use_id || randomUUID();
const toolName = event.name || 'unknown';
const toolInput = event.input || {};
// Prüfen ob dieses Tool einen Subagent startet
if (SUBAGENT_TOOLS.includes(toolName)) {
const subagentId = randomUUID();
const subagentType = getSubagentType(toolName, toolInput);
const subagentTask = toolInput.description || toolInput.prompt || toolInput.task || 'Subagent-Aufgabe';
const subagentModel = toolInput.model || useModel;
// Tiefe berechnen (Main = 0, erster Sub = 1, etc.)
// Für jetzt: immer depth 1 (direkter Subagent vom Main)
const depth = 1;
activeSubagents.set(toolId, {
agentId: subagentId,
parentId: currentAgentId,
type: subagentType,
task: subagentTask,
depth,
model: subagentModel,
});
sendEvent('subagent-started', {
id: subagentId,
parentAgentId: currentAgentId,
type: subagentType,
task: subagentTask.substring(0, 100),
depth,
model: subagentModel,
toolUseId: toolId,
});
}
case 'tool_use':
sendEvent('tool-start', {
id: toolId,
tool: toolName,
input: toolInput,
agentId: currentAgentId,
});
// Monitor: Tool gestartet
const toolSummary = summarizeToolInput(toolName, toolInput);
sendMonitorEvent('tool', `${toolName} ${toolSummary}`, {
toolId,
tool: toolName,
input: toolInput,
id: event.tool_use_id || randomUUID(),
tool: event.name || 'unknown',
input: event.input || {},
});
break;
}
case 'tool_result': {
const toolId = event.tool_use_id || '';
// Prüfen ob dieser Tool-Call ein Subagent war
if (activeSubagents.has(toolId)) {
const subagent = activeSubagents.get(toolId);
sendEvent('subagent-stopped', {
id: subagent.agentId,
parentAgentId: subagent.parentId,
success: !event.is_error,
toolUseId: toolId,
});
activeSubagents.delete(toolId);
}
case 'tool_result':
sendEvent('tool-end', {
id: toolId,
id: event.tool_use_id || '',
success: !event.is_error,
agentId: currentAgentId,
});
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,
tokens: { input: inputTokens, output: outputTokens },
cost: event.total_cost_usd || 0,
tokens: {
input: event.usage?.input_tokens || 0,
output: event.usage?.output_tokens || 0,
},
session_id: event.session_id || '',
duration_ms: durationMs,
duration_ms: Date.now() - startTime,
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
@ -289,29 +131,10 @@ 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
for (const [toolId, subagent] of activeSubagents) {
sendEvent('subagent-stopped', {
id: subagent.agentId,
parentAgentId: subagent.parentId,
success: false, // Vorzeitig beendet
toolUseId: toolId,
});
}
activeSubagents.clear();
sendEvent('agent-stopped', { id: currentAgentId, code: 0 });
sendEvent('all-stopped');
currentAgentId = null;

View file

@ -5,12 +5,6 @@
"windows": ["main"],
"permissions": [
"core:default",
"core:window:allow-show",
"core:window:allow-hide",
"core:window:allow-set-focus",
"core:window:allow-close",
"core:menu:default",
"core:tray:default",
"shell:allow-open",
"shell:allow-execute",
"shell:allow-spawn",

View file

@ -2,11 +2,7 @@
// Hauptmodul für die Rust-Seite der App
use std::sync::{Arc, Mutex};
use tauri::{
Manager,
menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
};
use tauri::Manager;
mod audit;
mod claude;
@ -101,57 +97,6 @@ pub fn run() {
println!("🧠 Initialisiere Gedächtnis-System...");
});
// Tray-Icon einrichten
let show_item = MenuItem::with_id(app, "show", "Fenster zeigen", true, None::<&str>)?;
let hide_item = MenuItem::with_id(app, "hide", "Minimieren", true, None::<&str>)?;
let quit_item = MenuItem::with_id(app, "quit", "Beenden", true, None::<&str>)?;
let tray_menu = Menu::with_items(app, &[&show_item, &hide_item, &quit_item])?;
// App-Icon für Tray verwenden
let icon = app.default_window_icon()
.cloned()
.expect("Kein App-Icon konfiguriert");
let tray_icon = TrayIconBuilder::new()
.icon(icon)
.menu(&tray_menu)
.show_menu_on_left_click(false)
.tooltip("Claude Desktop")
.on_menu_event(|app, event| {
match event.id.as_ref() {
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
"hide" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
}
}
"quit" => {
app.exit(0);
}
_ => {}
}
})
.on_tray_icon_event(|tray, event| {
// Doppelklick auf Tray-Icon zeigt das Fenster
if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } = event {
if let Some(window) = tray.app_handle().get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
})
.build(app)?;
// Tray-Icon Handle speichern (optional für späteren Zugriff)
app.manage(tray_icon);
println!("🔲 Tray-Icon eingerichtet");
Ok(())
})
.run(tauri::generate_context!())

View file

@ -31,10 +31,7 @@
},
"bundle": {
"active": true,
"targets": ["appimage", "deb"],
"icon": [
"icons/icon.png"
]
"targets": ["appimage", "deb"]
},
"plugins": {
"shell": {

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { agents, selectedAgentId, agentCount, agentTree, type AgentTreeNode } from '$lib/stores/app';
import { agents, selectedAgentId, agentCount } from '$lib/stores/app';
import type { Agent } from '$lib/stores/app';
// Status-Icons
@ -10,40 +10,14 @@
stopped: '🔴'
};
// Typ-Namen (erweitert für alle Agent-Typen)
// Typ-Namen
const typeNames: Record<Agent['type'], string> = {
main: 'Main Agent',
explore: 'Explore',
plan: 'Plan',
bash: 'Bash',
code: 'Code',
test: 'Test',
review: 'Review'
bash: 'Bash'
};
// Typ-Icons
const typeIcons: Record<Agent['type'], string> = {
main: '🤖',
explore: '🔍',
plan: '📋',
bash: '💻',
code: '✏️',
test: '🧪',
review: '👀'
};
// Collapsed State für Baumknoten
let collapsedNodes = new Set<string>();
function toggleCollapse(id: string) {
if (collapsedNodes.has(id)) {
collapsedNodes.delete(id);
} else {
collapsedNodes.add(id);
}
collapsedNodes = collapsedNodes; // Trigger reactivity
}
function selectAgent(id: string) {
$selectedAgentId = $selectedAgentId === id ? null : id;
}
@ -57,84 +31,13 @@
}
return `${seconds}s`;
}
// Rekursives Rendern von Baum-Knoten
function hasChildren(node: AgentTreeNode): boolean {
return node.children.length > 0;
}
</script>
<!-- Rekursive Komponente für Baum-Knoten -->
{#snippet agentNode(node: AgentTreeNode, depth: number)}
{@const agent = node.agent}
{@const isCollapsed = collapsedNodes.has(agent.id)}
{@const hasKids = hasChildren(node)}
<div class="agent-node" style="--depth: {depth}">
<!-- Verbindungslinie -->
{#if depth > 0}
<div class="tree-line"></div>
{/if}
<button
class="agent-item"
class:selected={$selectedAgentId === agent.id}
class:active={agent.status === 'active'}
class:is-subagent={depth > 0}
on:click={() => selectAgent(agent.id)}
>
<!-- Collapse-Toggle wenn Kinder vorhanden -->
{#if hasKids}
<button
class="collapse-toggle"
on:click|stopPropagation={() => toggleCollapse(agent.id)}
title={isCollapsed ? 'Aufklappen' : 'Zuklappen'}
>
{isCollapsed ? '▶' : '▼'}
</button>
{:else}
<span class="collapse-spacer"></span>
{/if}
<div class="agent-content">
<div class="agent-main">
<span class="agent-status">{statusIcons[agent.status]}</span>
<span class="agent-type-icon" title={typeNames[agent.type]}>{typeIcons[agent.type]}</span>
<span class="agent-type">{typeNames[agent.type]}</span>
{#if agent.model}
<span class="agent-model">{agent.model}</span>
{/if}
<span class="agent-duration">({formatDuration(agent.startedAt)})</span>
</div>
<div class="agent-task">{agent.task}</div>
<div class="agent-meta">
<span class="agent-tools">🔧 {agent.toolCalls.length}</span>
{#if depth > 0}
<span class="agent-depth">Ebene {depth}</span>
{/if}
</div>
</div>
</button>
<!-- Kinder rekursiv rendern -->
{#if hasKids && !isCollapsed}
<div class="agent-children">
{#each node.children as child}
{@render agentNode(child, depth + 1)}
{/each}
</div>
{/if}
</div>
{/snippet}
<div class="agent-view">
<div class="agent-header">
<h2>🤖 Agents</h2>
<h2>🤖 Agents & Sub-Agents</h2>
<div class="agent-summary">
{$agentCount.total} gesamt |
{$agentCount.mainAgents} Main |
{$agentCount.subAgents} Sub |
{$agentCount.active} aktiv
{$agentCount.total} gesamt | {$agentCount.active} aktiv
</div>
</div>
@ -144,9 +47,24 @@
<p class="hint">Agents erscheinen hier wenn Claude arbeitet.</p>
</div>
{:else}
<div class="agent-tree">
{#each $agentTree as rootNode}
{@render agentNode(rootNode, 0)}
<div class="agent-list">
{#each $agents as agent}
<button
class="agent-item"
class:selected={$selectedAgentId === agent.id}
class:active={agent.status === 'active'}
on:click={() => selectAgent(agent.id)}
>
<div class="agent-main">
<span class="agent-status">{statusIcons[agent.status]}</span>
<span class="agent-type">{typeNames[agent.type]}</span>
<span class="agent-duration">({formatDuration(agent.startedAt)})</span>
</div>
<div class="agent-task">{agent.task}</div>
<div class="agent-tools">
Tools: {agent.toolCalls.length} Aufrufe
</div>
</button>
{/each}
</div>
@ -155,8 +73,7 @@
{@const selectedAgent = $agents.find((a) => a.id === $selectedAgentId)}
{#if selectedAgent}
<div class="agent-details">
<h3>{typeIcons[selectedAgent.type]} {typeNames[selectedAgent.type]}</h3>
<h3>Details: {typeNames[selectedAgent.type]}</h3>
<div class="detail-row">
<span class="detail-label">Status:</span>
<span class="detail-value">{statusIcons[selectedAgent.status]} {selectedAgent.status}</span>
@ -165,12 +82,6 @@
<span class="detail-label">Aufgabe:</span>
<span class="detail-value">{selectedAgent.task}</span>
</div>
{#if selectedAgent.model}
<div class="detail-row">
<span class="detail-label">Modell:</span>
<span class="detail-value">{selectedAgent.model}</span>
</div>
{/if}
<div class="detail-row">
<span class="detail-label">Gestartet:</span>
<span class="detail-value">{selectedAgent.startedAt.toLocaleTimeString('de-DE')}</span>
@ -179,18 +90,6 @@
<span class="detail-label">Laufzeit:</span>
<span class="detail-value">{formatDuration(selectedAgent.startedAt)}</span>
</div>
{#if selectedAgent.parentAgentId}
<div class="detail-row">
<span class="detail-label">Parent:</span>
<span class="detail-value parent-link" on:click={() => selectAgent(selectedAgent.parentAgentId!)}>
{selectedAgent.parentAgentId.substring(0, 8)}...
</span>
</div>
{/if}
<div class="detail-row">
<span class="detail-label">Tiefe:</span>
<span class="detail-value">{selectedAgent.depth}</span>
</div>
<h4>Tool-Aufrufe ({selectedAgent.toolCalls.length})</h4>
<div class="tool-list">
@ -251,34 +150,13 @@
margin-top: var(--spacing-sm);
}
/* Baum-Ansicht */
.agent-tree {
.agent-list {
flex: 1;
overflow-y: auto;
padding: var(--spacing-sm);
}
.agent-node {
position: relative;
margin-left: calc(var(--depth) * 1.5rem);
}
/* Verbindungslinie für Subagents */
.tree-line {
position: absolute;
left: -1rem;
top: 0;
bottom: 50%;
width: 1rem;
border-left: 2px solid var(--bg-tertiary);
border-bottom: 2px solid var(--bg-tertiary);
border-bottom-left-radius: 4px;
}
.agent-item {
display: flex;
align-items: flex-start;
gap: var(--spacing-xs);
width: 100%;
text-align: left;
padding: var(--spacing-sm) var(--spacing-md);
@ -302,57 +180,15 @@
border-left: 3px solid var(--success);
}
.agent-item.is-subagent {
background: var(--bg-primary);
border-style: dashed;
}
.collapse-toggle {
padding: 2px 4px;
font-size: 0.625rem;
background: none;
border: none;
cursor: pointer;
color: var(--text-secondary);
flex-shrink: 0;
}
.collapse-toggle:hover {
color: var(--text-primary);
}
.collapse-spacer {
width: 18px;
flex-shrink: 0;
}
.agent-content {
flex: 1;
min-width: 0;
}
.agent-main {
display: flex;
align-items: center;
gap: var(--spacing-xs);
flex-wrap: wrap;
}
.agent-type-icon {
font-size: 0.875rem;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-xs);
}
.agent-type {
font-weight: 600;
font-size: 0.8rem;
}
.agent-model {
font-size: 0.625rem;
padding: 1px 4px;
background: var(--accent);
color: white;
border-radius: var(--radius-sm);
}
.agent-duration {
@ -366,27 +202,14 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: 2px;
}
.agent-meta {
display: flex;
gap: var(--spacing-sm);
.agent-tools {
font-size: 0.625rem;
color: var(--text-secondary);
margin-top: var(--spacing-xs);
}
.agent-depth {
padding: 1px 4px;
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
}
.agent-children {
margin-top: var(--spacing-xs);
}
/* Detail-Ansicht */
.agent-details {
padding: var(--spacing-md);
@ -419,15 +242,6 @@
min-width: 80px;
}
.parent-link {
cursor: pointer;
color: var(--accent);
}
.parent-link:hover {
text-decoration: underline;
}
.tool-list {
display: flex;
flex-direction: column;

View file

@ -1,22 +1,11 @@
<script lang="ts">
import { invoke } from '@tauri-apps/api/core';
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, type Message } from '$lib/stores/app';
import { marked, type Tokens } from 'marked';
import { marked } from 'marked';
import { tick, onDestroy } from 'svelte';
import { get } from 'svelte/store';
// Custom Renderer für Code-Blöcke mit Wrapper
const renderer = new marked.Renderer();
renderer.code = function ({ text, lang }: Tokens.Code): string {
const language = lang || '';
const escapedCode = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
return `<div class="code-block-wrapper" data-lang="${language}"><pre><code class="language-${language}">${escapedCode}</code></pre></div>`;
};
marked.setOptions({ breaks: true, gfm: true, renderer });
marked.setOptions({ breaks: true, gfm: true });
function renderMarkdown(text: string): string {
try {
@ -26,67 +15,8 @@
}
}
// Svelte Action: Copy-Buttons zu Code-Blöcken hinzufügen
function addCopyButtons(node: HTMLElement) {
function processCodeBlocks() {
const wrappers = node.querySelectorAll('.code-block-wrapper:not([data-copy-added])');
wrappers.forEach((wrapper) => {
wrapper.setAttribute('data-copy-added', 'true');
const lang = wrapper.getAttribute('data-lang') || '';
const codeEl = wrapper.querySelector('code');
const codeText = codeEl?.textContent || '';
// Header mit Sprache und Copy-Button erstellen
const header = document.createElement('div');
header.className = 'code-header';
if (lang) {
const langSpan = document.createElement('span');
langSpan.className = 'code-lang';
langSpan.textContent = lang;
header.appendChild(langSpan);
}
const copyBtn = document.createElement('button');
copyBtn.className = 'copy-btn';
copyBtn.title = 'Code kopieren';
copyBtn.innerHTML = '📋';
copyBtn.onclick = async () => {
try {
await navigator.clipboard.writeText(codeText);
copyBtn.innerHTML = '<span class="copied"></span>';
setTimeout(() => (copyBtn.innerHTML = '📋'), 2000);
} catch (err) {
console.error('Kopieren fehlgeschlagen:', err);
}
};
header.appendChild(copyBtn);
wrapper.insertBefore(header, wrapper.firstChild);
});
}
// Initial verarbeiten
processCodeBlocks();
// MutationObserver für dynamische Inhalte (Streaming)
const observer = new MutationObserver(processCodeBlocks);
observer.observe(node, { childList: true, subtree: true });
return {
destroy() {
observer.disconnect();
}
};
}
let messagesContainer: HTMLDivElement;
// Edit-Modus State
let editingMessageId: string | null = $state(null);
let editingContent: string = $state('');
async function scrollToBottom() {
await tick();
if (messagesContainer) {
@ -164,107 +94,6 @@
sendMessage();
}
}
// Edit-Funktionen
function startEdit(message: Message) {
editingMessageId = message.id;
editingContent = message.content;
}
function cancelEdit() {
editingMessageId = null;
editingContent = '';
}
async function confirmEdit() {
if (!editingMessageId || !editingContent.trim()) return;
const msgIndex = $messages.findIndex((m) => m.id === editingMessageId);
if (msgIndex === -1) return;
const updatedMessage = {
...$messages[msgIndex],
content: editingContent.trim(),
timestamp: new Date()
};
// Nachricht aktualisieren und alle folgenden entfernen
// (da sie auf der alten Nachricht basieren)
messages.update((msgs) => {
const newMsgs = [...msgs.slice(0, msgIndex), updatedMessage];
return newMsgs;
});
// Nachricht in DB aktualisieren
await saveMessageToDb(updatedMessage);
// Edit-Modus beenden
const newContent = editingContent.trim();
editingMessageId = null;
editingContent = '';
// Nachricht neu an Claude senden
$isProcessing = true;
try {
await invoke('send_message', { message: newContent });
} catch (err) {
console.error('Fehler beim Senden:', err);
addMessage('system', `Fehler: ${err}`);
$isProcessing = false;
}
}
function handleEditKeydown(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
confirmEdit();
} else if (event.key === 'Escape') {
cancelEdit();
}
}
// Regenerate: Antwort neu generieren
async function regenerateResponse(assistantMsgIndex: number) {
if ($isProcessing) return;
// Vorherige User-Nachricht finden
let userMsg: Message | null = null;
for (let i = assistantMsgIndex - 1; i >= 0; i--) {
if ($messages[i].role === 'user') {
userMsg = $messages[i];
break;
}
}
if (!userMsg) {
console.error('Keine User-Nachricht zum Regenerieren gefunden');
return;
}
// Alle Nachrichten ab der Assistant-Nachricht entfernen
messages.update((msgs) => msgs.slice(0, assistantMsgIndex));
// User-Nachricht erneut senden
$isProcessing = true;
try {
await invoke('send_message', { message: userMsg.content });
} catch (err) {
console.error('Fehler beim Regenerieren:', err);
addMessage('system', `Fehler: ${err}`);
$isProcessing = false;
}
}
// Prüfen ob eine Nachricht die letzte Assistant-Nachricht ist
function isLastAssistantMessage(index: number): boolean {
// Finde die letzte Assistant-Nachricht
for (let i = $messages.length - 1; i >= 0; i--) {
if ($messages[i].role === 'assistant' && $messages[i].content) {
return i === index;
}
}
return false;
}
</script>
<div class="chat-panel">
@ -273,7 +102,7 @@
<span class="msg-count">{$messages.length} Nachrichten</span>
</div>
<div class="chat-messages" bind:this={messagesContainer} use:addCopyButtons>
<div class="chat-messages" bind:this={messagesContainer}>
{#if $messages.length === 0}
<div class="empty-state">
<div class="empty-icon">🤖</div>
@ -281,8 +110,8 @@
<p class="hint">Enter = Senden, Shift+Enter = Neue Zeile, Escape = Stopp</p>
</div>
{:else}
{#each $messages as message, index}
<div class="message" class:user={message.role === 'user'} class:assistant={message.role === 'assistant'} class:system={message.role === 'system'} class:editing={editingMessageId === message.id}>
{#each $messages as message}
<div class="message" class:user={message.role === 'user'} class:assistant={message.role === 'assistant'} class:system={message.role === 'system'}>
<div class="message-header">
<span class="message-role">
{#if message.role === 'user'}
@ -293,33 +122,12 @@
⚙️ System
{/if}
</span>
<div class="message-actions">
{#if message.role === 'user' && !$isProcessing && editingMessageId !== message.id}
<button class="action-btn" onclick={() => startEdit(message)} title="Bearbeiten">✏️</button>
{/if}
{#if message.role === 'assistant' && !$isProcessing && isLastAssistantMessage(index) && message.content}
<button class="action-btn" onclick={() => regenerateResponse(index)} title="Antwort neu generieren">🔄</button>
{/if}
<span class="message-time">
{message.timestamp.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
</div>
<div class="message-content">
{#if editingMessageId === message.id}
<textarea
class="edit-textarea"
bind:value={editingContent}
onkeydown={handleEditKeydown}
rows="3"
></textarea>
<div class="edit-actions">
<button class="edit-btn cancel" onclick={cancelEdit}>Abbrechen</button>
<button class="edit-btn confirm" onclick={confirmEdit} disabled={!editingContent.trim()}>
Speichern & Senden
</button>
</div>
{:else if message.role === 'assistant'}
{#if message.role === 'assistant'}
{@html renderMarkdown(message.content)}
{:else}
{message.content}
@ -460,101 +268,11 @@
font-weight: 600;
}
.message-actions {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.action-btn {
padding: 0.15rem 0.3rem;
background: transparent;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
opacity: 0;
transition: all 0.15s ease;
font-size: 0.7rem;
}
.message:hover .action-btn {
opacity: 0.6;
}
.action-btn:hover {
opacity: 1 !important;
background: var(--bg-tertiary);
}
.message-time {
font-size: 0.6rem;
color: var(--text-secondary);
}
/* Edit-Modus */
.message.editing {
border: 1px solid var(--accent);
}
.edit-textarea {
width: 100%;
min-height: 60px;
padding: var(--spacing-sm);
font-size: 0.85rem;
line-height: 1.5;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
resize: vertical;
font-family: inherit;
}
.edit-textarea:focus {
outline: none;
border-color: var(--accent);
}
.edit-actions {
display: flex;
justify-content: flex-end;
gap: var(--spacing-sm);
margin-top: var(--spacing-xs);
}
.edit-btn {
padding: 0.3rem 0.75rem;
font-size: 0.75rem;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.15s ease;
}
.edit-btn.cancel {
background: transparent;
border: 1px solid var(--border);
color: var(--text-secondary);
}
.edit-btn.cancel:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.edit-btn.confirm {
background: var(--accent);
border: 1px solid var(--accent);
color: white;
}
.edit-btn.confirm:hover:not(:disabled) {
background: var(--accent-hover);
}
.edit-btn.confirm:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.message-content {
font-size: 0.85rem;
line-height: 1.6;
@ -581,68 +299,7 @@
border-radius: var(--radius-sm);
}
/* Code-Block mit Header und Copy-Button */
.message-content :global(.code-block-wrapper) {
margin: 0.5em 0;
border-radius: var(--radius-md);
background: var(--bg-tertiary);
overflow: hidden;
}
.message-content :global(.code-header) {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.3rem 0.75rem;
background: rgba(0, 0, 0, 0.15);
font-size: 0.65rem;
gap: 0.5rem;
}
.message-content :global(.code-lang) {
color: var(--text-secondary);
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.message-content :global(.copy-btn) {
margin-left: auto;
padding: 0.2rem 0.5rem;
background: transparent;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-secondary);
font-size: 0.7rem;
cursor: pointer;
transition: all 0.15s ease;
}
.message-content :global(.copy-btn:hover) {
background: var(--bg-secondary);
color: var(--text-primary);
border-color: var(--text-secondary);
}
.message-content :global(.copy-btn .copied) {
color: var(--success);
}
.message-content :global(.code-block-wrapper pre) {
margin: 0;
padding: var(--spacing-sm) var(--spacing-md);
overflow-x: auto;
font-size: 0.75rem;
line-height: 1.5;
}
.message-content :global(.code-block-wrapper pre code) {
padding: 0;
background: none;
}
/* Fallback für inline pre (ohne wrapper) */
.message-content :global(pre:not(.code-block-wrapper pre)) {
.message-content :global(pre) {
margin: 0.5em 0;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--bg-tertiary);
@ -652,7 +309,7 @@
line-height: 1.4;
}
.message-content :global(pre:not(.code-block-wrapper pre) code) {
.message-content :global(pre code) {
padding: 0;
background: none;
}

View file

@ -1,96 +0,0 @@
<script lang="ts">
// CodeBlock — Code mit Syntax-Highlight und Copy-Button
// Standalone-Komponente für manuellen Einsatz
interface Props {
code: string;
language?: string;
}
let { code, language = '' }: Props = $props();
let copied = $state(false);
async function copyToClipboard() {
try {
await navigator.clipboard.writeText(code);
copied = true;
setTimeout(() => (copied = false), 2000);
} catch (err) {
console.error('Kopieren fehlgeschlagen:', err);
}
}
</script>
<div class="code-block">
<div class="code-header">
{#if language}
<span class="code-lang">{language}</span>
{/if}
<button class="copy-btn" onclick={copyToClipboard} title="Code kopieren">
{#if copied}
<span class="copied">✓ Kopiert</span>
{:else}
<span>📋</span>
{/if}
</button>
</div>
<pre><code class="language-{language}">{code}</code></pre>
</div>
<style>
.code-block {
position: relative;
margin: 0.5em 0;
border-radius: var(--radius-md);
background: var(--bg-tertiary);
overflow: hidden;
}
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.25rem 0.75rem;
background: rgba(0, 0, 0, 0.2);
font-size: 0.65rem;
}
.code-lang {
color: var(--text-secondary);
font-family: var(--font-mono);
text-transform: uppercase;
}
.copy-btn {
padding: 0.2rem 0.5rem;
background: transparent;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-secondary);
font-size: 0.7rem;
cursor: pointer;
transition: all 0.15s ease;
}
.copy-btn:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
.copied {
color: var(--success);
}
pre {
margin: 0;
padding: var(--spacing-sm) var(--spacing-md);
overflow-x: auto;
font-size: 0.75rem;
line-height: 1.5;
}
code {
font-family: var(--font-mono);
}
</style>

View file

@ -1,438 +0,0 @@
<script lang="ts">
import {
filteredMonitorEvents,
monitorFilter,
monitorAutoScroll,
selectedMonitorEventId,
monitorStats,
monitorEventColors,
clearMonitorEvents,
type MonitorEvent,
type MonitorEventType
} from '$lib/stores/app';
import { tick } from 'svelte';
// Event-Liste Container für Auto-Scroll
let eventListEl: HTMLDivElement;
// Auto-Scroll bei neuen Events
$effect(() => {
if ($monitorAutoScroll && $filteredMonitorEvents.length > 0) {
tick().then(() => {
if (eventListEl) {
eventListEl.scrollTop = eventListEl.scrollHeight;
}
});
}
});
// Event auswählen
function selectEvent(id: string) {
$selectedMonitorEventId = $selectedMonitorEventId === id ? null : id;
}
// Zeit formatieren
function formatTime(date: Date): string {
return date.toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3
});
}
// Details formatieren
function formatDetails(details: Record<string, unknown>): string {
return JSON.stringify(details, null, 2);
}
// Event-Typ Labels
const typeLabels: Record<MonitorEventType, string> = {
api: 'API',
hook: 'HOOK',
tool: 'TOOL',
mcp: 'MCP',
agent: 'AGENT',
error: 'ERROR',
debug: 'DEBUG',
};
// Filter-Optionen
const filterOptions: Array<{ value: MonitorEventType | 'all'; label: string }> = [
{ value: 'all', label: 'Alle' },
{ value: 'api', label: '🔵 API' },
{ value: 'tool', label: '🟡 Tools' },
{ value: 'agent', label: '🟠 Agents' },
{ value: 'mcp', label: '🟣 MCP' },
{ value: 'hook', label: '🟢 Hooks' },
{ value: 'error', label: '🔴 Fehler' },
{ value: 'debug', label: '⚪ Debug' },
];
</script>
<div class="monitor-panel">
<!-- Header mit Stats und Filter -->
<div class="monitor-header">
<div class="monitor-title">
<h2>📊 System-Monitor</h2>
<div class="monitor-stats">
<span class="stat" title="Alle Events">{$monitorStats.totalEvents}</span>
<span class="stat api" title="API-Calls">{$monitorStats.apiCalls} API</span>
{#if $monitorStats.errors > 0}
<span class="stat error" title="Fehler">{$monitorStats.errors} Err</span>
{/if}
{#if $monitorStats.avgLatencyMs > 0}
<span class="stat latency" title="Durchschnittliche Latenz">{$monitorStats.avgLatencyMs}ms</span>
{/if}
</div>
</div>
<div class="monitor-controls">
<select bind:value={$monitorFilter} class="filter-select">
{#each filterOptions as opt}
<option value={opt.value}>{opt.label}</option>
{/each}
</select>
<label class="auto-scroll-toggle" title="Auto-Scroll">
<input type="checkbox" bind:checked={$monitorAutoScroll} />
<span></span>
</label>
<button class="clear-btn" on:click={clearMonitorEvents} title="Events löschen">
🗑️
</button>
</div>
</div>
<!-- Event-Liste -->
<div class="event-list" bind:this={eventListEl}>
{#if $filteredMonitorEvents.length === 0}
<div class="empty-state">
<p>Keine Events.</p>
<p class="hint">Events erscheinen hier wenn Claude arbeitet.</p>
</div>
{:else}
{#each $filteredMonitorEvents as event (event.id)}
<button
class="event-item"
class:selected={$selectedMonitorEventId === event.id}
class:error={event.type === 'error'}
on:click={() => selectEvent(event.id)}
>
<span class="event-time">{formatTime(event.timestamp)}</span>
<span class="event-icon">{monitorEventColors[event.type]}</span>
<span class="event-type">{typeLabels[event.type]}</span>
<span class="event-summary">{event.summary}</span>
{#if event.durationMs}
<span class="event-duration">[{event.durationMs}ms]</span>
{/if}
</button>
{/each}
{/if}
</div>
<!-- Detail-Ansicht -->
{#if $selectedMonitorEventId}
{@const selectedEvent = $filteredMonitorEvents.find((e) => e.id === $selectedMonitorEventId)}
{#if selectedEvent}
<div class="event-details">
<div class="details-header">
<h3>
{monitorEventColors[selectedEvent.type]} {typeLabels[selectedEvent.type]}
</h3>
<button class="copy-btn" on:click={() => navigator.clipboard.writeText(formatDetails(selectedEvent.details))} title="Kopieren">
📋
</button>
</div>
<div class="details-row">
<span class="label">Zeit:</span>
<span class="value">{selectedEvent.timestamp.toLocaleString('de-DE')}</span>
</div>
<div class="details-row">
<span class="label">Summary:</span>
<span class="value">{selectedEvent.summary}</span>
</div>
{#if selectedEvent.durationMs}
<div class="details-row">
<span class="label">Dauer:</span>
<span class="value">{selectedEvent.durationMs}ms</span>
</div>
{/if}
{#if selectedEvent.agentId}
<div class="details-row">
<span class="label">Agent:</span>
<span class="value mono">{selectedEvent.agentId.substring(0, 8)}...</span>
</div>
{/if}
{#if selectedEvent.error}
<div class="details-row error">
<span class="label">Fehler:</span>
<span class="value">{selectedEvent.error}</span>
</div>
{/if}
<h4>Details</h4>
<pre class="details-json">{formatDetails(selectedEvent.details)}</pre>
</div>
{/if}
{/if}
</div>
<style>
.monitor-panel {
display: flex;
flex-direction: column;
height: 100%;
font-family: var(--font-mono, monospace);
font-size: 0.75rem;
}
.monitor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--bg-secondary);
border-bottom: 1px solid var(--bg-tertiary);
flex-wrap: wrap;
gap: var(--spacing-xs);
}
.monitor-title {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.monitor-title h2 {
font-size: 0.875rem;
font-weight: 600;
margin: 0;
}
.monitor-stats {
display: flex;
gap: var(--spacing-xs);
}
.stat {
padding: 2px 6px;
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
font-size: 0.625rem;
}
.stat.api {
color: #4a9eff;
}
.stat.error {
background: var(--error);
color: white;
}
.stat.latency {
color: var(--text-secondary);
}
.monitor-controls {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.filter-select {
padding: 4px 8px;
font-size: 0.75rem;
background: var(--bg-tertiary);
border: 1px solid var(--bg-tertiary);
border-radius: var(--radius-sm);
color: var(--text-primary);
}
.auto-scroll-toggle {
display: flex;
align-items: center;
cursor: pointer;
}
.auto-scroll-toggle input {
display: none;
}
.auto-scroll-toggle span {
padding: 4px 8px;
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
opacity: 0.5;
}
.auto-scroll-toggle input:checked + span {
opacity: 1;
background: var(--accent);
color: white;
}
.clear-btn {
padding: 4px 8px;
background: var(--bg-tertiary);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
}
.clear-btn:hover {
background: var(--error);
}
.event-list {
flex: 1;
overflow-y: auto;
padding: var(--spacing-xs);
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-secondary);
text-align: center;
}
.empty-state .hint {
font-size: 0.625rem;
margin-top: var(--spacing-xs);
}
.event-item {
display: flex;
align-items: center;
gap: var(--spacing-xs);
width: 100%;
text-align: left;
padding: 4px var(--spacing-sm);
margin-bottom: 2px;
background: var(--bg-secondary);
border: 1px solid transparent;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.1s ease;
}
.event-item:hover {
background: var(--bg-tertiary);
}
.event-item.selected {
border-color: var(--accent);
}
.event-item.error {
background: rgba(239, 68, 68, 0.1);
}
.event-time {
color: var(--text-secondary);
font-size: 0.625rem;
flex-shrink: 0;
}
.event-icon {
flex-shrink: 0;
}
.event-type {
font-weight: 600;
flex-shrink: 0;
min-width: 50px;
}
.event-summary {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-secondary);
}
.event-duration {
font-size: 0.625rem;
color: var(--text-secondary);
flex-shrink: 0;
}
/* Detail-Ansicht */
.event-details {
padding: var(--spacing-sm) var(--spacing-md);
background: var(--bg-secondary);
border-top: 1px solid var(--bg-tertiary);
max-height: 40%;
overflow-y: auto;
}
.details-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-sm);
}
.details-header h3 {
font-size: 0.875rem;
margin: 0;
}
.copy-btn {
padding: 4px 8px;
background: var(--bg-tertiary);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
}
.copy-btn:hover {
background: var(--accent);
}
.details-row {
display: flex;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-xs);
}
.details-row .label {
color: var(--text-secondary);
min-width: 60px;
}
.details-row .value {
flex: 1;
}
.details-row .mono {
font-family: var(--font-mono, monospace);
}
.details-row.error .value {
color: var(--error);
}
.event-details h4 {
font-size: 0.75rem;
margin: var(--spacing-sm) 0 var(--spacing-xs);
}
.details-json {
padding: var(--spacing-sm);
background: var(--bg-primary);
border-radius: var(--radius-sm);
overflow-x: auto;
font-size: 0.625rem;
max-height: 200px;
overflow-y: auto;
}
</style>

View file

@ -5,15 +5,11 @@ import { writable, derived } from 'svelte/store';
// Typen
export interface Agent {
id: string;
type: 'main' | 'explore' | 'plan' | 'bash' | 'code' | 'test' | 'review';
type: 'main' | 'explore' | 'plan' | 'bash';
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 {
@ -78,34 +74,9 @@ 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,
idle: $agents.filter((a) => a.status === 'idle').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) => [
@ -120,27 +91,8 @@ export function addMessage(role: Message['role'], content: string, agentId?: str
]);
}
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;
}
})();
}
export function addAgent(type: Agent['type'], task: string): string {
const id = crypto.randomUUID();
agents.update((ags) => [
...ags,
{
@ -149,30 +101,12 @@ export function addAgent(type: Agent['type'], task: string, options?: AddAgentOp
status: 'active',
task,
startedAt: new Date(),
toolCalls: [],
parentAgentId,
depth,
model: options?.model,
toolCalls: []
}
]);
return id;
}
// Subagent hinzufügen (Kurzform)
export function addSubAgent(
parentId: string,
type: Agent['type'],
task: string,
options?: Omit<AddAgentOptions, 'parentAgentId'>
): 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))
@ -261,108 +195,3 @@ 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<string, unknown>; // Vollständige Daten
sessionId?: string;
agentId?: string;
durationMs?: number;
error?: string;
}
// Farbcodierung für Event-Typen
export const monitorEventColors: Record<MonitorEventType, string> = {
api: '🔵',
hook: '🟢',
tool: '🟡',
mcp: '🟣',
agent: '🟠',
error: '🔴',
debug: '⚪',
};
// Monitor Store — Ringbuffer mit max 1000 Events
const MAX_MONITOR_EVENTS = 1000;
export const monitorEvents = writable<MonitorEvent[]>([]);
// Filter für Monitor-Ansicht
export const monitorFilter = writable<MonitorEventType | 'all'>('all');
export const monitorAutoScroll = writable(true);
export const selectedMonitorEventId = writable<string | null>(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<string, unknown> = {},
options?: Partial<Omit<MonitorEvent, 'id' | 'timestamp' | 'type' | 'summary' | 'details'>>
) {
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, '***@***.***');
}

View file

@ -11,7 +11,6 @@ import {
isProcessing,
addMessage,
addAgent,
addSubAgent,
updateAgentStatus,
addToolCall,
completeToolCall,
@ -20,10 +19,7 @@ import {
sessionStats,
currentSessionId,
messageToDb,
addMonitorEvent,
type Message,
type Agent,
type MonitorEventType
type Message
} from './app';
// Event-Typen vom Backend
@ -32,18 +28,6 @@ interface AgentEvent {
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 {
@ -68,15 +52,6 @@ interface ResultEvent {
model?: string;
}
interface MonitorEventPayload {
type: MonitorEventType;
summary: string;
details: Record<string, unknown>;
agentId?: string;
durationMs?: number;
error?: string;
}
// Listener-Handles
let listeners: UnlistenFn[] = [];
@ -162,30 +137,6 @@ export async function initEventListeners(): Promise<void> {
})
);
// 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', (event) => {
@ -278,19 +229,6 @@ export async function initEventListeners(): Promise<void> {
})
);
// 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');
}
@ -303,25 +241,17 @@ export async function cleanupEventListeners(): Promise<void> {
}
// Agent-Typ mappen
function mapAgentType(type: string): Agent['type'] {
const typeMap: Record<string, Agent['type']> = {
function mapAgentType(type: string): 'main' | 'explore' | 'plan' | 'bash' {
const typeMap: Record<string, 'main' | 'explore' | 'plan' | 'bash'> = {
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',
Bash: 'bash'
};
return typeMap[type] || 'explore';
return typeMap[type] || 'main';
}

View file

@ -8,14 +8,12 @@
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: '📝' },
];
@ -62,8 +60,6 @@
<div class="panel-content">
{#if activeMiddleTab === 'activity'}
<ActivityPanel />
{:else if activeMiddleTab === 'monitor'}
<MonitorPanel />
{:else if activeMiddleTab === 'memory'}
<MemoryPanel />
{:else if activeMiddleTab === 'audit'}