Compare commits
8 commits
4ba14a53e1
...
9d837efae6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d837efae6 | ||
|
|
adb11fd121 | ||
|
|
6cfcdb2c79 | ||
|
|
18c8ef2f4f | ||
|
|
f640b18f47 | ||
|
|
25617dc76f | ||
|
|
35872f8679 | ||
|
|
3c6da3b3d5 |
12 changed files with 2913 additions and 64 deletions
1300
ROADMAP.md
Normal file
1300
ROADMAP.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -19,6 +19,10 @@ let activeAbort = null;
|
||||||
let currentAgentId = null;
|
let currentAgentId = null;
|
||||||
let currentModel = process.env.CLAUDE_MODEL || 'opus';
|
let currentModel = process.env.CLAUDE_MODEL || 'opus';
|
||||||
|
|
||||||
|
// Subagent-Tracking
|
||||||
|
// Map: toolUseId → { agentId, parentId, type, task, depth }
|
||||||
|
const activeSubagents = new Map();
|
||||||
|
|
||||||
// Verfügbare Modelle
|
// Verfügbare Modelle
|
||||||
const AVAILABLE_MODELS = [
|
const AVAILABLE_MODELS = [
|
||||||
{ id: 'haiku', name: 'Claude Haiku', description: 'Schnell & günstig' },
|
{ id: 'haiku', name: 'Claude Haiku', description: 'Schnell & günstig' },
|
||||||
|
|
@ -26,6 +30,26 @@ const AVAILABLE_MODELS = [
|
||||||
{ id: 'opus', name: 'Claude Opus', description: 'Leistungsstark' },
|
{ 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 ============
|
// ============ Kommunikation mit Tauri ============
|
||||||
|
|
||||||
function sendToTauri(msg) {
|
function sendToTauri(msg) {
|
||||||
|
|
@ -44,6 +68,52 @@ function sendError(id, error) {
|
||||||
sendToTauri({ type: 'response', 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 ============
|
// ============ Claude Agent SDK ============
|
||||||
|
|
||||||
async function sendMessage(message, requestId, model = null) {
|
async function sendMessage(message, requestId, model = null) {
|
||||||
|
|
@ -60,6 +130,20 @@ async function sendMessage(message, requestId, model = null) {
|
||||||
model: useModel,
|
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 });
|
sendResponse(requestId, { agentId: currentAgentId, status: 'gestartet', model: useModel });
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
@ -93,35 +177,109 @@ async function sendMessage(message, requestId, model = null) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'tool_use':
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
sendEvent('tool-start', {
|
sendEvent('tool-start', {
|
||||||
id: event.tool_use_id || randomUUID(),
|
id: toolId,
|
||||||
tool: event.name || 'unknown',
|
tool: toolName,
|
||||||
input: event.input || {},
|
input: toolInput,
|
||||||
|
agentId: currentAgentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Monitor: Tool gestartet
|
||||||
|
const toolSummary = summarizeToolInput(toolName, toolInput);
|
||||||
|
sendMonitorEvent('tool', `${toolName} ${toolSummary}`, {
|
||||||
|
toolId,
|
||||||
|
tool: toolName,
|
||||||
|
input: toolInput,
|
||||||
});
|
});
|
||||||
break;
|
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', {
|
sendEvent('tool-end', {
|
||||||
id: event.tool_use_id || '',
|
id: toolId,
|
||||||
success: !event.is_error,
|
success: !event.is_error,
|
||||||
|
agentId: currentAgentId,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'result':
|
case 'result': {
|
||||||
// Endergebnis
|
// 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', {
|
sendEvent('result', {
|
||||||
text: fullText,
|
text: fullText,
|
||||||
cost: event.total_cost_usd || 0,
|
cost,
|
||||||
tokens: {
|
tokens: { input: inputTokens, output: outputTokens },
|
||||||
input: event.usage?.input_tokens || 0,
|
|
||||||
output: event.usage?.output_tokens || 0,
|
|
||||||
},
|
|
||||||
session_id: event.session_id || '',
|
session_id: event.session_id || '',
|
||||||
duration_ms: Date.now() - startTime,
|
duration_ms: durationMs,
|
||||||
model: usedModel,
|
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;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Andere Events still ignorieren
|
// Andere Events still ignorieren
|
||||||
|
|
@ -131,10 +289,29 @@ async function sendMessage(message, requestId, model = null) {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.name === 'AbortError') {
|
if (err.name === 'AbortError') {
|
||||||
// Abgebrochen — kein Fehler
|
// Abgebrochen — kein Fehler
|
||||||
|
sendMonitorEvent('agent', 'Abgebrochen (User)', { reason: 'abort' });
|
||||||
} else {
|
} else {
|
||||||
sendEvent('text', { text: `\n\n**Fehler:** ${err.message || err}` });
|
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 {
|
} 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('agent-stopped', { id: currentAgentId, code: 0 });
|
||||||
sendEvent('all-stopped');
|
sendEvent('all-stopped');
|
||||||
currentAgentId = null;
|
currentAgentId = null;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,12 @@
|
||||||
"windows": ["main"],
|
"windows": ["main"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"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-open",
|
||||||
"shell:allow-execute",
|
"shell:allow-execute",
|
||||||
"shell:allow-spawn",
|
"shell:allow-spawn",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@
|
||||||
// Hauptmodul für die Rust-Seite der App
|
// Hauptmodul für die Rust-Seite der App
|
||||||
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tauri::Manager;
|
use tauri::{
|
||||||
|
Manager,
|
||||||
|
menu::{Menu, MenuItem},
|
||||||
|
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||||
|
};
|
||||||
|
|
||||||
mod audit;
|
mod audit;
|
||||||
mod claude;
|
mod claude;
|
||||||
|
|
@ -97,6 +101,57 @@ pub fn run() {
|
||||||
println!("🧠 Initialisiere Gedächtnis-System...");
|
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(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,10 @@
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": ["appimage", "deb"]
|
"targets": ["appimage", "deb"],
|
||||||
|
"icon": [
|
||||||
|
"icons/icon.png"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"shell": {
|
"shell": {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { agents, selectedAgentId, agentCount } from '$lib/stores/app';
|
import { agents, selectedAgentId, agentCount, agentTree, type AgentTreeNode } from '$lib/stores/app';
|
||||||
import type { Agent } from '$lib/stores/app';
|
import type { Agent } from '$lib/stores/app';
|
||||||
|
|
||||||
// Status-Icons
|
// Status-Icons
|
||||||
|
|
@ -10,14 +10,40 @@
|
||||||
stopped: '🔴'
|
stopped: '🔴'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Typ-Namen
|
// Typ-Namen (erweitert für alle Agent-Typen)
|
||||||
const typeNames: Record<Agent['type'], string> = {
|
const typeNames: Record<Agent['type'], string> = {
|
||||||
main: 'Main Agent',
|
main: 'Main Agent',
|
||||||
explore: 'Explore',
|
explore: 'Explore',
|
||||||
plan: 'Plan',
|
plan: 'Plan',
|
||||||
bash: 'Bash'
|
bash: 'Bash',
|
||||||
|
code: 'Code',
|
||||||
|
test: 'Test',
|
||||||
|
review: 'Review'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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) {
|
function selectAgent(id: string) {
|
||||||
$selectedAgentId = $selectedAgentId === id ? null : id;
|
$selectedAgentId = $selectedAgentId === id ? null : id;
|
||||||
}
|
}
|
||||||
|
|
@ -31,13 +57,84 @@
|
||||||
}
|
}
|
||||||
return `${seconds}s`;
|
return `${seconds}s`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rekursives Rendern von Baum-Knoten
|
||||||
|
function hasChildren(node: AgentTreeNode): boolean {
|
||||||
|
return node.children.length > 0;
|
||||||
|
}
|
||||||
</script>
|
</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-view">
|
||||||
<div class="agent-header">
|
<div class="agent-header">
|
||||||
<h2>🤖 Agents & Sub-Agents</h2>
|
<h2>🤖 Agents</h2>
|
||||||
<div class="agent-summary">
|
<div class="agent-summary">
|
||||||
{$agentCount.total} gesamt | {$agentCount.active} aktiv
|
{$agentCount.total} gesamt |
|
||||||
|
{$agentCount.mainAgents} Main |
|
||||||
|
{$agentCount.subAgents} Sub |
|
||||||
|
{$agentCount.active} aktiv
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -47,24 +144,9 @@
|
||||||
<p class="hint">Agents erscheinen hier wenn Claude arbeitet.</p>
|
<p class="hint">Agents erscheinen hier wenn Claude arbeitet.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="agent-list">
|
<div class="agent-tree">
|
||||||
{#each $agents as agent}
|
{#each $agentTree as rootNode}
|
||||||
<button
|
{@render agentNode(rootNode, 0)}
|
||||||
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}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -73,7 +155,8 @@
|
||||||
{@const selectedAgent = $agents.find((a) => a.id === $selectedAgentId)}
|
{@const selectedAgent = $agents.find((a) => a.id === $selectedAgentId)}
|
||||||
{#if selectedAgent}
|
{#if selectedAgent}
|
||||||
<div class="agent-details">
|
<div class="agent-details">
|
||||||
<h3>Details: {typeNames[selectedAgent.type]}</h3>
|
<h3>{typeIcons[selectedAgent.type]} {typeNames[selectedAgent.type]}</h3>
|
||||||
|
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<span class="detail-label">Status:</span>
|
<span class="detail-label">Status:</span>
|
||||||
<span class="detail-value">{statusIcons[selectedAgent.status]} {selectedAgent.status}</span>
|
<span class="detail-value">{statusIcons[selectedAgent.status]} {selectedAgent.status}</span>
|
||||||
|
|
@ -82,6 +165,12 @@
|
||||||
<span class="detail-label">Aufgabe:</span>
|
<span class="detail-label">Aufgabe:</span>
|
||||||
<span class="detail-value">{selectedAgent.task}</span>
|
<span class="detail-value">{selectedAgent.task}</span>
|
||||||
</div>
|
</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">
|
<div class="detail-row">
|
||||||
<span class="detail-label">Gestartet:</span>
|
<span class="detail-label">Gestartet:</span>
|
||||||
<span class="detail-value">{selectedAgent.startedAt.toLocaleTimeString('de-DE')}</span>
|
<span class="detail-value">{selectedAgent.startedAt.toLocaleTimeString('de-DE')}</span>
|
||||||
|
|
@ -90,6 +179,18 @@
|
||||||
<span class="detail-label">Laufzeit:</span>
|
<span class="detail-label">Laufzeit:</span>
|
||||||
<span class="detail-value">{formatDuration(selectedAgent.startedAt)}</span>
|
<span class="detail-value">{formatDuration(selectedAgent.startedAt)}</span>
|
||||||
</div>
|
</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>
|
<h4>Tool-Aufrufe ({selectedAgent.toolCalls.length})</h4>
|
||||||
<div class="tool-list">
|
<div class="tool-list">
|
||||||
|
|
@ -150,13 +251,34 @@
|
||||||
margin-top: var(--spacing-sm);
|
margin-top: var(--spacing-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-list {
|
/* Baum-Ansicht */
|
||||||
|
.agent-tree {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: var(--spacing-sm);
|
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 {
|
.agent-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: var(--spacing-sm) var(--spacing-md);
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
|
@ -180,15 +302,57 @@
|
||||||
border-left: 3px solid var(--success);
|
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 {
|
.agent-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-sm);
|
gap: var(--spacing-xs);
|
||||||
margin-bottom: var(--spacing-xs);
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-type-icon {
|
||||||
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-type {
|
.agent-type {
|
||||||
font-weight: 600;
|
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 {
|
.agent-duration {
|
||||||
|
|
@ -202,14 +366,27 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-tools {
|
.agent-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
font-size: 0.625rem;
|
font-size: 0.625rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
margin-top: var(--spacing-xs);
|
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 */
|
/* Detail-Ansicht */
|
||||||
.agent-details {
|
.agent-details {
|
||||||
padding: var(--spacing-md);
|
padding: var(--spacing-md);
|
||||||
|
|
@ -242,6 +419,15 @@
|
||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.parent-link {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.parent-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.tool-list {
|
.tool-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,22 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, type Message } from '$lib/stores/app';
|
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, type Message } from '$lib/stores/app';
|
||||||
import { marked } from 'marked';
|
import { marked, type Tokens } from 'marked';
|
||||||
import { tick, onDestroy } from 'svelte';
|
import { tick, onDestroy } from 'svelte';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
marked.setOptions({ breaks: true, gfm: true });
|
// 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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
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 });
|
||||||
|
|
||||||
function renderMarkdown(text: string): string {
|
function renderMarkdown(text: string): string {
|
||||||
try {
|
try {
|
||||||
|
|
@ -15,8 +26,67 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
let messagesContainer: HTMLDivElement;
|
||||||
|
|
||||||
|
// Edit-Modus State
|
||||||
|
let editingMessageId: string | null = $state(null);
|
||||||
|
let editingContent: string = $state('');
|
||||||
|
|
||||||
async function scrollToBottom() {
|
async function scrollToBottom() {
|
||||||
await tick();
|
await tick();
|
||||||
if (messagesContainer) {
|
if (messagesContainer) {
|
||||||
|
|
@ -94,6 +164,107 @@
|
||||||
sendMessage();
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="chat-panel">
|
<div class="chat-panel">
|
||||||
|
|
@ -102,7 +273,7 @@
|
||||||
<span class="msg-count">{$messages.length} Nachrichten</span>
|
<span class="msg-count">{$messages.length} Nachrichten</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chat-messages" bind:this={messagesContainer}>
|
<div class="chat-messages" bind:this={messagesContainer} use:addCopyButtons>
|
||||||
{#if $messages.length === 0}
|
{#if $messages.length === 0}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-icon">🤖</div>
|
<div class="empty-icon">🤖</div>
|
||||||
|
|
@ -110,8 +281,8 @@
|
||||||
<p class="hint">Enter = Senden, Shift+Enter = Neue Zeile, Escape = Stopp</p>
|
<p class="hint">Enter = Senden, Shift+Enter = Neue Zeile, Escape = Stopp</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each $messages as message}
|
{#each $messages as message, index}
|
||||||
<div class="message" class:user={message.role === 'user'} class:assistant={message.role === 'assistant'} class:system={message.role === 'system'}>
|
<div class="message" class:user={message.role === 'user'} class:assistant={message.role === 'assistant'} class:system={message.role === 'system'} class:editing={editingMessageId === message.id}>
|
||||||
<div class="message-header">
|
<div class="message-header">
|
||||||
<span class="message-role">
|
<span class="message-role">
|
||||||
{#if message.role === 'user'}
|
{#if message.role === 'user'}
|
||||||
|
|
@ -122,12 +293,33 @@
|
||||||
⚙️ System
|
⚙️ System
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
<span class="message-time">
|
<div class="message-actions">
|
||||||
{message.timestamp.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
|
{#if message.role === 'user' && !$isProcessing && editingMessageId !== message.id}
|
||||||
</span>
|
<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>
|
||||||
<div class="message-content">
|
<div class="message-content">
|
||||||
{#if message.role === 'assistant'}
|
{#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'}
|
||||||
{@html renderMarkdown(message.content)}
|
{@html renderMarkdown(message.content)}
|
||||||
{:else}
|
{:else}
|
||||||
{message.content}
|
{message.content}
|
||||||
|
|
@ -268,11 +460,101 @@
|
||||||
font-weight: 600;
|
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 {
|
.message-time {
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
color: var(--text-secondary);
|
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 {
|
.message-content {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
|
@ -299,7 +581,68 @@
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content :global(pre) {
|
/* 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)) {
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
padding: var(--spacing-sm) var(--spacing-md);
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
|
|
@ -309,7 +652,7 @@
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content :global(pre code) {
|
.message-content :global(pre:not(.code-block-wrapper pre) code) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
96
src/lib/components/CodeBlock.svelte
Normal file
96
src/lib/components/CodeBlock.svelte
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
<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>
|
||||||
438
src/lib/components/MonitorPanel.svelte
Normal file
438
src/lib/components/MonitorPanel.svelte
Normal file
|
|
@ -0,0 +1,438 @@
|
||||||
|
<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>
|
||||||
|
|
@ -5,11 +5,15 @@ import { writable, derived } from 'svelte/store';
|
||||||
// Typen
|
// Typen
|
||||||
export interface Agent {
|
export interface Agent {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'main' | 'explore' | 'plan' | 'bash';
|
type: 'main' | 'explore' | 'plan' | 'bash' | 'code' | 'test' | 'review';
|
||||||
status: 'active' | 'waiting' | 'idle' | 'stopped';
|
status: 'active' | 'waiting' | 'idle' | 'stopped';
|
||||||
task: string;
|
task: string;
|
||||||
startedAt: Date;
|
startedAt: Date;
|
||||||
toolCalls: ToolCall[];
|
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 {
|
export interface ToolCall {
|
||||||
|
|
@ -74,9 +78,34 @@ export const agentCount = derived(agents, ($agents) => ({
|
||||||
total: $agents.length,
|
total: $agents.length,
|
||||||
active: $agents.filter((a) => a.status === 'active').length,
|
active: $agents.filter((a) => a.status === 'active').length,
|
||||||
waiting: $agents.filter((a) => a.status === 'waiting').length,
|
waiting: $agents.filter((a) => a.status === 'waiting').length,
|
||||||
idle: $agents.filter((a) => a.status === 'idle').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
|
// Aktionen
|
||||||
export function addMessage(role: Message['role'], content: string, agentId?: string) {
|
export function addMessage(role: Message['role'], content: string, agentId?: string) {
|
||||||
messages.update((msgs) => [
|
messages.update((msgs) => [
|
||||||
|
|
@ -91,8 +120,27 @@ export function addMessage(role: Message['role'], content: string, agentId?: str
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addAgent(type: Agent['type'], task: string): string {
|
export interface AddAgentOptions {
|
||||||
const id = crypto.randomUUID();
|
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) => [
|
agents.update((ags) => [
|
||||||
...ags,
|
...ags,
|
||||||
{
|
{
|
||||||
|
|
@ -101,12 +149,30 @@ export function addAgent(type: Agent['type'], task: string): string {
|
||||||
status: 'active',
|
status: 'active',
|
||||||
task,
|
task,
|
||||||
startedAt: new Date(),
|
startedAt: new Date(),
|
||||||
toolCalls: []
|
toolCalls: [],
|
||||||
|
parentAgentId,
|
||||||
|
depth,
|
||||||
|
model: options?.model,
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
return id;
|
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']) {
|
export function updateAgentStatus(id: string, status: Agent['status']) {
|
||||||
agents.update((ags) =>
|
agents.update((ags) =>
|
||||||
ags.map((a) => (a.id === id ? { ...a, status } : a))
|
ags.map((a) => (a.id === id ? { ...a, status } : a))
|
||||||
|
|
@ -195,3 +261,108 @@ export function dbToMessage(db: DbMessage): Message {
|
||||||
export function setMessagesFromDb(dbMessages: DbMessage[]) {
|
export function setMessagesFromDb(dbMessages: DbMessage[]) {
|
||||||
messages.set(dbMessages.map(dbToMessage));
|
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, '***@***.***');
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
isProcessing,
|
isProcessing,
|
||||||
addMessage,
|
addMessage,
|
||||||
addAgent,
|
addAgent,
|
||||||
|
addSubAgent,
|
||||||
updateAgentStatus,
|
updateAgentStatus,
|
||||||
addToolCall,
|
addToolCall,
|
||||||
completeToolCall,
|
completeToolCall,
|
||||||
|
|
@ -19,7 +20,10 @@ import {
|
||||||
sessionStats,
|
sessionStats,
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
messageToDb,
|
messageToDb,
|
||||||
type Message
|
addMonitorEvent,
|
||||||
|
type Message,
|
||||||
|
type Agent,
|
||||||
|
type MonitorEventType
|
||||||
} from './app';
|
} from './app';
|
||||||
|
|
||||||
// Event-Typen vom Backend
|
// Event-Typen vom Backend
|
||||||
|
|
@ -28,6 +32,18 @@ interface AgentEvent {
|
||||||
type?: string;
|
type?: string;
|
||||||
task?: string;
|
task?: string;
|
||||||
code?: number;
|
code?: number;
|
||||||
|
model?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SubagentEvent {
|
||||||
|
id: string;
|
||||||
|
parentAgentId: string;
|
||||||
|
type?: string;
|
||||||
|
task?: string;
|
||||||
|
depth?: number;
|
||||||
|
model?: string;
|
||||||
|
toolUseId?: string;
|
||||||
|
success?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToolEvent {
|
interface ToolEvent {
|
||||||
|
|
@ -52,6 +68,15 @@ interface ResultEvent {
|
||||||
model?: string;
|
model?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MonitorEventPayload {
|
||||||
|
type: MonitorEventType;
|
||||||
|
summary: string;
|
||||||
|
details: Record<string, unknown>;
|
||||||
|
agentId?: string;
|
||||||
|
durationMs?: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Listener-Handles
|
// Listener-Handles
|
||||||
let listeners: UnlistenFn[] = [];
|
let listeners: UnlistenFn[] = [];
|
||||||
|
|
||||||
|
|
@ -137,6 +162,30 @@ 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
|
// Tool Start
|
||||||
listeners.push(
|
listeners.push(
|
||||||
await listen<ToolEvent>('tool-start', (event) => {
|
await listen<ToolEvent>('tool-start', (event) => {
|
||||||
|
|
@ -229,6 +278,19 @@ 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');
|
console.log('✅ Event-Listener initialisiert');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,17 +303,25 @@ export async function cleanupEventListeners(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agent-Typ mappen
|
// Agent-Typ mappen
|
||||||
function mapAgentType(type: string): 'main' | 'explore' | 'plan' | 'bash' {
|
function mapAgentType(type: string): Agent['type'] {
|
||||||
const typeMap: Record<string, 'main' | 'explore' | 'plan' | 'bash'> = {
|
const typeMap: Record<string, Agent['type']> = {
|
||||||
main: 'main',
|
main: 'main',
|
||||||
'Main Agent': 'main',
|
'Main Agent': 'main',
|
||||||
Main: 'main',
|
Main: 'main',
|
||||||
explore: 'explore',
|
explore: 'explore',
|
||||||
Explore: 'explore',
|
Explore: 'explore',
|
||||||
|
'general-purpose': 'explore',
|
||||||
plan: 'plan',
|
plan: 'plan',
|
||||||
Plan: 'plan',
|
Plan: 'plan',
|
||||||
bash: 'bash',
|
bash: 'bash',
|
||||||
Bash: 'bash'
|
Bash: 'bash',
|
||||||
|
code: 'code',
|
||||||
|
Code: 'code',
|
||||||
|
implement: 'code',
|
||||||
|
test: 'test',
|
||||||
|
Test: 'test',
|
||||||
|
review: 'review',
|
||||||
|
Review: 'review',
|
||||||
};
|
};
|
||||||
return typeMap[type] || 'main';
|
return typeMap[type] || 'explore';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,14 @@
|
||||||
import AuditLog from '$lib/components/AuditLog.svelte';
|
import AuditLog from '$lib/components/AuditLog.svelte';
|
||||||
import GuardRailsPanel from '$lib/components/GuardRailsPanel.svelte';
|
import GuardRailsPanel from '$lib/components/GuardRailsPanel.svelte';
|
||||||
import SettingsPanel from '$lib/components/SettingsPanel.svelte';
|
import SettingsPanel from '$lib/components/SettingsPanel.svelte';
|
||||||
|
import MonitorPanel from '$lib/components/MonitorPanel.svelte';
|
||||||
|
|
||||||
let activeMiddleTab = 'activity';
|
let activeMiddleTab = 'activity';
|
||||||
let activeRightTab = 'agents';
|
let activeRightTab = 'agents';
|
||||||
|
|
||||||
const middleTabs = [
|
const middleTabs = [
|
||||||
{ id: 'activity', label: 'Aktivität', icon: '📋' },
|
{ id: 'activity', label: 'Aktivität', icon: '📋' },
|
||||||
|
{ id: 'monitor', label: 'Monitor', icon: '📊' },
|
||||||
{ id: 'memory', label: 'Gedächtnis', icon: '🧠' },
|
{ id: 'memory', label: 'Gedächtnis', icon: '🧠' },
|
||||||
{ id: 'audit', label: 'Historie', icon: '📝' },
|
{ id: 'audit', label: 'Historie', icon: '📝' },
|
||||||
];
|
];
|
||||||
|
|
@ -60,6 +62,8 @@
|
||||||
<div class="panel-content">
|
<div class="panel-content">
|
||||||
{#if activeMiddleTab === 'activity'}
|
{#if activeMiddleTab === 'activity'}
|
||||||
<ActivityPanel />
|
<ActivityPanel />
|
||||||
|
{:else if activeMiddleTab === 'monitor'}
|
||||||
|
<MonitorPanel />
|
||||||
{:else if activeMiddleTab === 'memory'}
|
{:else if activeMiddleTab === 'memory'}
|
||||||
<MemoryPanel />
|
<MemoryPanel />
|
||||||
{:else if activeMiddleTab === 'audit'}
|
{:else if activeMiddleTab === 'audit'}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue