Compare commits
No commits in common. "9d837efae69093c93781e38a354118dc58493682" and "4ba14a53e19830eb1b4b9df90c87b4e4a9dddd4d" have entirely different histories.
9d837efae6
...
4ba14a53e1
12 changed files with 64 additions and 2913 deletions
1300
ROADMAP.md
1300
ROADMAP.md
File diff suppressed because it is too large
Load diff
|
|
@ -19,10 +19,6 @@ 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' },
|
||||||
|
|
@ -30,26 +26,6 @@ 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) {
|
||||||
|
|
@ -68,52 +44,6 @@ 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) {
|
||||||
|
|
@ -130,20 +60,6 @@ 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();
|
||||||
|
|
@ -177,109 +93,35 @@ 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: toolId,
|
id: event.tool_use_id || randomUUID(),
|
||||||
tool: toolName,
|
tool: event.name || 'unknown',
|
||||||
input: toolInput,
|
input: event.input || {},
|
||||||
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: toolId,
|
id: event.tool_use_id || '',
|
||||||
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,
|
cost: event.total_cost_usd || 0,
|
||||||
tokens: { input: inputTokens, output: outputTokens },
|
tokens: {
|
||||||
|
input: event.usage?.input_tokens || 0,
|
||||||
|
output: event.usage?.output_tokens || 0,
|
||||||
|
},
|
||||||
session_id: event.session_id || '',
|
session_id: event.session_id || '',
|
||||||
duration_ms: durationMs,
|
duration_ms: Date.now() - startTime,
|
||||||
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
|
||||||
|
|
@ -289,29 +131,10 @@ 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,12 +5,6 @@
|
||||||
"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,11 +2,7 @@
|
||||||
// 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::{
|
use tauri::Manager;
|
||||||
Manager,
|
|
||||||
menu::{Menu, MenuItem},
|
|
||||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
|
||||||
};
|
|
||||||
|
|
||||||
mod audit;
|
mod audit;
|
||||||
mod claude;
|
mod claude;
|
||||||
|
|
@ -101,57 +97,6 @@ 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,10 +31,7 @@
|
||||||
},
|
},
|
||||||
"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, agentTree, type AgentTreeNode } from '$lib/stores/app';
|
import { agents, selectedAgentId, agentCount } from '$lib/stores/app';
|
||||||
import type { Agent } from '$lib/stores/app';
|
import type { Agent } from '$lib/stores/app';
|
||||||
|
|
||||||
// Status-Icons
|
// Status-Icons
|
||||||
|
|
@ -10,40 +10,14 @@
|
||||||
stopped: '🔴'
|
stopped: '🔴'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Typ-Namen (erweitert für alle Agent-Typen)
|
// Typ-Namen
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
@ -57,84 +31,13 @@
|
||||||
}
|
}
|
||||||
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</h2>
|
<h2>🤖 Agents & Sub-Agents</h2>
|
||||||
<div class="agent-summary">
|
<div class="agent-summary">
|
||||||
{$agentCount.total} gesamt |
|
{$agentCount.total} gesamt | {$agentCount.active} aktiv
|
||||||
{$agentCount.mainAgents} Main |
|
|
||||||
{$agentCount.subAgents} Sub |
|
|
||||||
{$agentCount.active} aktiv
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -144,9 +47,24 @@
|
||||||
<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-tree">
|
<div class="agent-list">
|
||||||
{#each $agentTree as rootNode}
|
{#each $agents as agent}
|
||||||
{@render agentNode(rootNode, 0)}
|
<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}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -155,8 +73,7 @@
|
||||||
{@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>{typeIcons[selectedAgent.type]} {typeNames[selectedAgent.type]}</h3>
|
<h3>Details: {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>
|
||||||
|
|
@ -165,12 +82,6 @@
|
||||||
<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>
|
||||||
|
|
@ -179,18 +90,6 @@
|
||||||
<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">
|
||||||
|
|
@ -251,34 +150,13 @@
|
||||||
margin-top: var(--spacing-sm);
|
margin-top: var(--spacing-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Baum-Ansicht */
|
.agent-list {
|
||||||
.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);
|
||||||
|
|
@ -302,57 +180,15 @@
|
||||||
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-xs);
|
gap: var(--spacing-sm);
|
||||||
flex-wrap: wrap;
|
margin-bottom: var(--spacing-xs);
|
||||||
}
|
|
||||||
|
|
||||||
.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 {
|
||||||
|
|
@ -366,27 +202,14 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
margin-top: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-meta {
|
.agent-tools {
|
||||||
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);
|
||||||
|
|
@ -419,15 +242,6 @@
|
||||||
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,22 +1,11 @@
|
||||||
<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, type Tokens } from 'marked';
|
import { marked } from 'marked';
|
||||||
import { tick, onDestroy } from 'svelte';
|
import { tick, onDestroy } from 'svelte';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
// Custom Renderer für Code-Blöcke mit Wrapper
|
marked.setOptions({ breaks: true, gfm: true });
|
||||||
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 {
|
||||||
|
|
@ -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;
|
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) {
|
||||||
|
|
@ -164,107 +94,6 @@
|
||||||
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">
|
||||||
|
|
@ -273,7 +102,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} use:addCopyButtons>
|
<div class="chat-messages" bind:this={messagesContainer}>
|
||||||
{#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>
|
||||||
|
|
@ -281,8 +110,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, index}
|
{#each $messages as message}
|
||||||
<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" class:user={message.role === 'user'} class:assistant={message.role === 'assistant'} class:system={message.role === 'system'}>
|
||||||
<div class="message-header">
|
<div class="message-header">
|
||||||
<span class="message-role">
|
<span class="message-role">
|
||||||
{#if message.role === 'user'}
|
{#if message.role === 'user'}
|
||||||
|
|
@ -293,33 +122,12 @@
|
||||||
⚙️ System
|
⚙️ System
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
<div class="message-actions">
|
<span class="message-time">
|
||||||
{#if message.role === 'user' && !$isProcessing && editingMessageId !== message.id}
|
{message.timestamp.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
|
||||||
<button class="action-btn" onclick={() => startEdit(message)} title="Bearbeiten">✏️</button>
|
</span>
|
||||||
{/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 editingMessageId === message.id}
|
{#if message.role === 'assistant'}
|
||||||
<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}
|
||||||
|
|
@ -460,101 +268,11 @@
|
||||||
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;
|
||||||
|
|
@ -581,68 +299,7 @@
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Code-Block mit Header und Copy-Button */
|
.message-content :global(pre) {
|
||||||
.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);
|
||||||
|
|
@ -652,7 +309,7 @@
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content :global(pre:not(.code-block-wrapper pre) code) {
|
.message-content :global(pre code) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -5,15 +5,11 @@ import { writable, derived } from 'svelte/store';
|
||||||
// Typen
|
// Typen
|
||||||
export interface Agent {
|
export interface Agent {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'main' | 'explore' | 'plan' | 'bash' | 'code' | 'test' | 'review';
|
type: 'main' | 'explore' | 'plan' | 'bash';
|
||||||
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 {
|
||||||
|
|
@ -78,34 +74,9 @@ 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) => [
|
||||||
|
|
@ -120,27 +91,8 @@ export function addMessage(role: Message['role'], content: string, agentId?: str
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddAgentOptions {
|
export function addAgent(type: Agent['type'], task: string): string {
|
||||||
id?: string;
|
const id = crypto.randomUUID();
|
||||||
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,
|
||||||
{
|
{
|
||||||
|
|
@ -149,30 +101,12 @@ export function addAgent(type: Agent['type'], task: string, options?: AddAgentOp
|
||||||
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))
|
||||||
|
|
@ -261,108 +195,3 @@ 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,7 +11,6 @@ import {
|
||||||
isProcessing,
|
isProcessing,
|
||||||
addMessage,
|
addMessage,
|
||||||
addAgent,
|
addAgent,
|
||||||
addSubAgent,
|
|
||||||
updateAgentStatus,
|
updateAgentStatus,
|
||||||
addToolCall,
|
addToolCall,
|
||||||
completeToolCall,
|
completeToolCall,
|
||||||
|
|
@ -20,10 +19,7 @@ import {
|
||||||
sessionStats,
|
sessionStats,
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
messageToDb,
|
messageToDb,
|
||||||
addMonitorEvent,
|
type Message
|
||||||
type Message,
|
|
||||||
type Agent,
|
|
||||||
type MonitorEventType
|
|
||||||
} from './app';
|
} from './app';
|
||||||
|
|
||||||
// Event-Typen vom Backend
|
// Event-Typen vom Backend
|
||||||
|
|
@ -32,18 +28,6 @@ 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 {
|
||||||
|
|
@ -68,15 +52,6 @@ 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[] = [];
|
||||||
|
|
||||||
|
|
@ -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
|
// Tool Start
|
||||||
listeners.push(
|
listeners.push(
|
||||||
await listen<ToolEvent>('tool-start', (event) => {
|
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');
|
console.log('✅ Event-Listener initialisiert');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -303,25 +241,17 @@ export async function cleanupEventListeners(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agent-Typ mappen
|
// Agent-Typ mappen
|
||||||
function mapAgentType(type: string): Agent['type'] {
|
function mapAgentType(type: string): 'main' | 'explore' | 'plan' | 'bash' {
|
||||||
const typeMap: Record<string, Agent['type']> = {
|
const typeMap: Record<string, 'main' | 'explore' | 'plan' | 'bash'> = {
|
||||||
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] || 'explore';
|
return typeMap[type] || 'main';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,12 @@
|
||||||
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: '📝' },
|
||||||
];
|
];
|
||||||
|
|
@ -62,8 +60,6 @@
|
||||||
<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