Phase 2: Claude SDK Integration + Event-System
- claude-bridge.js: Node.js Bridge für Claude Code CLI - claude.rs: Child-Process Management, Event-Verarbeitung - events.ts: Frontend Event-Listener für Tauri-Events - Layout/ChatPanel: Echte Tauri-Commands statt Placeholder Events implementiert: - agent-started/stopped - tool-start/tool-end - claude-text (Streaming) - claude-result (Kosten/Token) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2822796c7a
commit
5003fb9996
7 changed files with 673 additions and 25 deletions
193
src-tauri/scripts/claude-bridge.js
Normal file
193
src-tauri/scripts/claude-bridge.js
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
#!/usr/bin/env node
|
||||
// Claude Desktop — Bridge zu Claude Code SDK
|
||||
// Kommuniziert mit Rust-Backend über stdin/stdout (JSON)
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const readline = require('readline');
|
||||
|
||||
// State
|
||||
let claudeProcess = null;
|
||||
let abortController = null;
|
||||
|
||||
// Event an Rust senden
|
||||
function emit(event, payload) {
|
||||
const msg = JSON.stringify({ type: 'event', event, payload });
|
||||
process.stdout.write(msg + '\n');
|
||||
}
|
||||
|
||||
// Antwort an Rust senden
|
||||
function respond(id, result, error = null) {
|
||||
const msg = JSON.stringify({ type: 'response', id, result, error });
|
||||
process.stdout.write(msg + '\n');
|
||||
}
|
||||
|
||||
// Claude Code als Subprocess starten
|
||||
async function startClaude(message, requestId) {
|
||||
abortController = new AbortController();
|
||||
|
||||
emit('agent-started', {
|
||||
id: 'main',
|
||||
type: 'Main Agent',
|
||||
task: message.substring(0, 100)
|
||||
});
|
||||
|
||||
try {
|
||||
// Claude Code CLI aufrufen
|
||||
claudeProcess = spawn('claude', [
|
||||
'--output-format', 'stream-json',
|
||||
'-p', message
|
||||
], {
|
||||
signal: abortController.signal,
|
||||
env: { ...process.env, FORCE_COLOR: '0' }
|
||||
});
|
||||
|
||||
let fullResponse = '';
|
||||
let buffer = '';
|
||||
|
||||
claudeProcess.stdout.on('data', (data) => {
|
||||
buffer += data.toString();
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop(); // Unvollständige Zeile behalten
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const event = JSON.parse(line);
|
||||
handleClaudeEvent(event);
|
||||
if (event.type === 'assistant' && event.message?.content) {
|
||||
for (const block of event.message.content) {
|
||||
if (block.type === 'text') {
|
||||
fullResponse += block.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Kein JSON, ignorieren
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
claudeProcess.stderr.on('data', (data) => {
|
||||
emit('log', { level: 'error', message: data.toString() });
|
||||
});
|
||||
|
||||
claudeProcess.on('close', (code) => {
|
||||
emit('agent-stopped', { id: 'main', code });
|
||||
respond(requestId, fullResponse || 'Keine Antwort erhalten');
|
||||
claudeProcess = null;
|
||||
abortController = null;
|
||||
});
|
||||
|
||||
claudeProcess.on('error', (err) => {
|
||||
if (err.name === 'AbortError') {
|
||||
respond(requestId, null, 'Abgebrochen durch Benutzer');
|
||||
} else {
|
||||
respond(requestId, null, err.message);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
respond(requestId, null, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Claude SDK Events verarbeiten
|
||||
function handleClaudeEvent(event) {
|
||||
switch (event.type) {
|
||||
case 'tool_use':
|
||||
emit('tool-start', {
|
||||
id: event.tool_use_id,
|
||||
tool: event.name,
|
||||
input: event.input
|
||||
});
|
||||
break;
|
||||
|
||||
case 'tool_result':
|
||||
emit('tool-end', {
|
||||
id: event.tool_use_id,
|
||||
success: !event.is_error,
|
||||
output: typeof event.content === 'string'
|
||||
? event.content.substring(0, 500)
|
||||
: JSON.stringify(event.content).substring(0, 500)
|
||||
});
|
||||
break;
|
||||
|
||||
case 'subagent_start':
|
||||
emit('subagent-start', {
|
||||
id: event.subagent_id,
|
||||
type: event.subagent_type,
|
||||
task: event.prompt?.substring(0, 100)
|
||||
});
|
||||
break;
|
||||
|
||||
case 'subagent_stop':
|
||||
emit('subagent-stop', {
|
||||
id: event.subagent_id
|
||||
});
|
||||
break;
|
||||
|
||||
case 'assistant':
|
||||
if (event.message?.content) {
|
||||
for (const block of event.message.content) {
|
||||
if (block.type === 'text') {
|
||||
emit('text', { text: block.text });
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'result':
|
||||
emit('result', {
|
||||
cost: event.cost_usd,
|
||||
tokens: {
|
||||
input: event.input_tokens,
|
||||
output: event.output_tokens
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Alle Prozesse stoppen
|
||||
function stopAll() {
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
}
|
||||
if (claudeProcess) {
|
||||
claudeProcess.kill('SIGTERM');
|
||||
}
|
||||
emit('all-stopped', {});
|
||||
}
|
||||
|
||||
// Stdin lesen
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
terminal: false
|
||||
});
|
||||
|
||||
rl.on('line', (line) => {
|
||||
try {
|
||||
const msg = JSON.parse(line);
|
||||
|
||||
switch (msg.command) {
|
||||
case 'message':
|
||||
startClaude(msg.message, msg.id);
|
||||
break;
|
||||
case 'stop':
|
||||
stopAll();
|
||||
respond(msg.id, 'stopped');
|
||||
break;
|
||||
case 'ping':
|
||||
respond(msg.id, 'pong');
|
||||
break;
|
||||
default:
|
||||
respond(msg.id, null, `Unbekannter Befehl: ${msg.command}`);
|
||||
}
|
||||
} catch (e) {
|
||||
emit('error', { message: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Startup
|
||||
emit('ready', { version: '0.1.0' });
|
||||
|
|
@ -2,7 +2,10 @@
|
|||
// Kommunikation mit Claude Code via Node.js Child-Process
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
|
||||
/// Status eines Agents
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -14,18 +17,238 @@ pub struct AgentStatus {
|
|||
pub tool_calls: u32,
|
||||
}
|
||||
|
||||
/// Tool-Aufruf Event
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolEvent {
|
||||
pub id: String,
|
||||
pub tool: Option<String>,
|
||||
pub input: Option<serde_json::Value>,
|
||||
pub output: Option<String>,
|
||||
pub success: Option<bool>,
|
||||
}
|
||||
|
||||
/// Agent Event
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentEvent {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub agent_type: Option<String>,
|
||||
pub task: Option<String>,
|
||||
pub code: Option<i32>,
|
||||
}
|
||||
|
||||
/// Nachricht von der Bridge
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct BridgeMessage {
|
||||
#[serde(rename = "type")]
|
||||
msg_type: String,
|
||||
event: Option<String>,
|
||||
payload: Option<serde_json::Value>,
|
||||
id: Option<String>,
|
||||
result: Option<serde_json::Value>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
/// Globaler State für die Bridge
|
||||
pub struct ClaudeState {
|
||||
pub bridge_stdin: Option<std::process::ChildStdin>,
|
||||
pub request_counter: u64,
|
||||
pub agents: Vec<AgentStatus>,
|
||||
}
|
||||
|
||||
impl Default for ClaudeState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bridge_stdin: None,
|
||||
request_counter: 0,
|
||||
agents: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bridge starten
|
||||
pub fn start_bridge(app: &AppHandle) -> Result<(), String> {
|
||||
// Script-Pfad ermitteln
|
||||
let exe_dir = std::env::current_exe()
|
||||
.map_err(|e| e.to_string())?
|
||||
.parent()
|
||||
.ok_or("Kein Parent-Verzeichnis")?
|
||||
.to_path_buf();
|
||||
|
||||
let script_path = exe_dir.join("scripts").join("claude-bridge.js");
|
||||
let script_path = if script_path.exists() {
|
||||
script_path
|
||||
} else {
|
||||
// Fallback für Entwicklung
|
||||
std::path::PathBuf::from("scripts/claude-bridge.js")
|
||||
};
|
||||
|
||||
println!("🔌 Starte Claude Bridge: {:?}", script_path);
|
||||
|
||||
let mut child = Command::new("node")
|
||||
.arg(&script_path)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("Bridge konnte nicht gestartet werden: {}", e))?;
|
||||
|
||||
let stdin = child.stdin.take().ok_or("Kein stdin verfügbar")?;
|
||||
let stdout = child.stdout.take().ok_or("Kein stdout verfügbar")?;
|
||||
|
||||
// State speichern
|
||||
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
||||
{
|
||||
let mut state = state.lock().unwrap();
|
||||
state.bridge_stdin = Some(stdin);
|
||||
}
|
||||
|
||||
// Stdout in separatem Thread lesen
|
||||
let app_handle = app.clone();
|
||||
std::thread::spawn(move || {
|
||||
let reader = BufReader::new(stdout);
|
||||
for line in reader.lines().map_while(Result::ok) {
|
||||
if let Ok(msg) = serde_json::from_str::<BridgeMessage>(&line) {
|
||||
handle_bridge_message(&app_handle, msg);
|
||||
}
|
||||
}
|
||||
println!("⚠️ Bridge stdout geschlossen");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bridge-Nachrichten verarbeiten
|
||||
fn handle_bridge_message(app: &AppHandle, msg: BridgeMessage) {
|
||||
match msg.msg_type.as_str() {
|
||||
"event" => {
|
||||
if let (Some(event), Some(payload)) = (msg.event, msg.payload) {
|
||||
match event.as_str() {
|
||||
"ready" => {
|
||||
println!("✅ Claude Bridge bereit");
|
||||
let _ = app.emit("bridge-ready", ());
|
||||
}
|
||||
"agent-started" | "subagent-start" => {
|
||||
if let Ok(agent) = serde_json::from_value::<AgentEvent>(payload.clone()) {
|
||||
println!("🤖 Agent gestartet: {}", agent.id);
|
||||
let _ = app.emit("agent-started", &payload);
|
||||
|
||||
// Agent zur Liste hinzufügen
|
||||
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
||||
let mut state = state.lock().unwrap();
|
||||
state.agents.push(AgentStatus {
|
||||
id: agent.id,
|
||||
agent_type: agent.agent_type.unwrap_or_else(|| "Main".to_string()),
|
||||
status: "active".to_string(),
|
||||
task: agent.task.unwrap_or_default(),
|
||||
tool_calls: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
"agent-stopped" | "subagent-stop" => {
|
||||
if let Ok(agent) = serde_json::from_value::<AgentEvent>(payload.clone()) {
|
||||
println!("⏹️ Agent gestoppt: {}", agent.id);
|
||||
let _ = app.emit("agent-stopped", &payload);
|
||||
|
||||
// Agent aus Liste entfernen
|
||||
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
||||
let mut state = state.lock().unwrap();
|
||||
state.agents.retain(|a| a.id != agent.id);
|
||||
}
|
||||
}
|
||||
"tool-start" => {
|
||||
if let Ok(tool) = serde_json::from_value::<ToolEvent>(payload.clone()) {
|
||||
println!("🔧 Tool Start: {} - {:?}", tool.id, tool.tool);
|
||||
let _ = app.emit("tool-start", &payload);
|
||||
}
|
||||
}
|
||||
"tool-end" => {
|
||||
if let Ok(tool) = serde_json::from_value::<ToolEvent>(payload.clone()) {
|
||||
println!(
|
||||
"✅ Tool Ende: {} - {}",
|
||||
tool.id,
|
||||
if tool.success.unwrap_or(true) { "OK" } else { "FEHLER" }
|
||||
);
|
||||
let _ = app.emit("tool-end", &payload);
|
||||
}
|
||||
}
|
||||
"text" => {
|
||||
let _ = app.emit("claude-text", &payload);
|
||||
}
|
||||
"result" => {
|
||||
let _ = app.emit("claude-result", &payload);
|
||||
}
|
||||
"all-stopped" => {
|
||||
println!("⏹️ Alle Agents gestoppt");
|
||||
let _ = app.emit("all-stopped", ());
|
||||
|
||||
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
||||
let mut state = state.lock().unwrap();
|
||||
state.agents.clear();
|
||||
}
|
||||
_ => {
|
||||
println!("📨 Event: {} = {:?}", event, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"response" => {
|
||||
if let Some(id) = msg.id {
|
||||
println!("📬 Response für {}: {:?}", id, msg.result);
|
||||
// TODO: Über Channel an wartenden Request weitergeben
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Befehl an Bridge senden
|
||||
fn send_to_bridge(app: &AppHandle, command: &str, message: &str) -> Result<String, String> {
|
||||
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
||||
let mut state = state.lock().unwrap();
|
||||
|
||||
state.request_counter += 1;
|
||||
let request_id = format!("req-{}", state.request_counter);
|
||||
|
||||
let msg = serde_json::json!({
|
||||
"command": command,
|
||||
"id": request_id,
|
||||
"message": message
|
||||
});
|
||||
|
||||
if let Some(stdin) = &mut state.bridge_stdin {
|
||||
writeln!(stdin, "{}", msg.to_string()).map_err(|e| e.to_string())?;
|
||||
stdin.flush().map_err(|e| e.to_string())?;
|
||||
Ok(request_id)
|
||||
} else {
|
||||
Err("Bridge nicht gestartet".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Tauri Commands ============
|
||||
|
||||
/// Nachricht an Claude senden
|
||||
#[tauri::command]
|
||||
pub async fn send_message(
|
||||
app: AppHandle,
|
||||
message: String,
|
||||
) -> Result<String, String> {
|
||||
pub async fn send_message(app: AppHandle, message: String) -> Result<String, String> {
|
||||
println!("📨 Nachricht empfangen: {}", &message[..message.len().min(50)]);
|
||||
|
||||
// TODO: Claude SDK über Node.js Child-Process aufrufen
|
||||
// Vorläufig: Placeholder-Antwort
|
||||
// Bridge starten falls nicht aktiv
|
||||
let needs_start = {
|
||||
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
||||
let state_guard = state.lock().unwrap();
|
||||
state_guard.bridge_stdin.is_none()
|
||||
};
|
||||
|
||||
Ok("Claude SDK noch nicht verbunden. Integration folgt in Phase 1.3.".to_string())
|
||||
if needs_start {
|
||||
start_bridge(&app)?;
|
||||
// Kurz warten bis Bridge bereit
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
}
|
||||
|
||||
send_to_bridge(&app, "message", &message)?;
|
||||
|
||||
// Hinweis: Die eigentliche Antwort kommt über Events
|
||||
Ok("Nachricht gesendet. Antwort folgt über Events.".to_string())
|
||||
}
|
||||
|
||||
/// Alle Agents stoppen
|
||||
|
|
@ -33,9 +256,7 @@ pub async fn send_message(
|
|||
pub async fn stop_all_agents(app: AppHandle) -> Result<(), String> {
|
||||
println!("⏹️ STOPP: Alle Agents werden gestoppt");
|
||||
|
||||
// TODO: AbortController für alle laufenden Prozesse triggern
|
||||
|
||||
// Event an Frontend senden
|
||||
let _ = send_to_bridge(&app, "stop", "");
|
||||
app.emit("agents-stopped", ()).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
|
|
@ -43,8 +264,8 @@ pub async fn stop_all_agents(app: AppHandle) -> Result<(), String> {
|
|||
|
||||
/// Status aller Agents abrufen
|
||||
#[tauri::command]
|
||||
pub async fn get_agent_status() -> Result<Vec<AgentStatus>, String> {
|
||||
// TODO: Echte Agent-Daten zurückgeben
|
||||
|
||||
Ok(vec![])
|
||||
pub async fn get_agent_status(app: AppHandle) -> Result<Vec<AgentStatus>, String> {
|
||||
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
||||
let state = state.lock().unwrap();
|
||||
Ok(state.agents.clone())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
// Claude Desktop — Tauri Backend
|
||||
// Hauptmodul für die Rust-Seite der App
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::Manager;
|
||||
|
||||
mod audit;
|
||||
mod claude;
|
||||
mod memory;
|
||||
mod audit;
|
||||
|
||||
/// Initialisiert die App
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.manage(Arc::new(Mutex::new(claude::ClaudeState::default())))
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// Claude SDK
|
||||
claude::send_message,
|
||||
|
|
@ -38,6 +40,9 @@ pub fn run() {
|
|||
// TODO: memory::load_memory aufrufen
|
||||
});
|
||||
|
||||
// Bridge optional beim Start starten (oder lazy bei erster Nachricht)
|
||||
// let _ = claude::start_bridge(&app.handle());
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { messages, currentInput, isProcessing, addMessage } from '$lib/stores/app';
|
||||
|
||||
async function sendMessage() {
|
||||
|
|
@ -10,12 +11,15 @@
|
|||
$currentInput = '';
|
||||
$isProcessing = true;
|
||||
|
||||
// TODO: An Claude senden via Tauri
|
||||
// Placeholder-Antwort
|
||||
setTimeout(() => {
|
||||
addMessage('assistant', 'Ich bin noch nicht mit dem Claude SDK verbunden. Die Integration folgt in Phase 1.3.');
|
||||
try {
|
||||
// An Claude senden via Tauri
|
||||
await invoke('send_message', { message: text });
|
||||
// Antwort kommt über Events (claude-text, agent-stopped)
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Senden:', err);
|
||||
addMessage('system', `Fehler: ${err}`);
|
||||
$isProcessing = false;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
|
|
@ -39,10 +43,16 @@
|
|||
</div>
|
||||
{:else}
|
||||
{#each $messages as message}
|
||||
<div class="message" class:user={message.role === 'user'} class:assistant={message.role === 'assistant'}>
|
||||
<div class="message" class:user={message.role === 'user'} class:assistant={message.role === 'assistant'} class:system={message.role === 'system'}>
|
||||
<div class="message-header">
|
||||
<span class="message-role">
|
||||
{message.role === 'user' ? '👤 Du' : '🤖 Claude'}
|
||||
{#if message.role === 'user'}
|
||||
👤 Du
|
||||
{:else if message.role === 'assistant'}
|
||||
🤖 Claude
|
||||
{:else}
|
||||
⚙️ System
|
||||
{/if}
|
||||
</span>
|
||||
<span class="message-time">
|
||||
{message.timestamp.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
|
||||
|
|
@ -144,6 +154,11 @@
|
|||
margin-right: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.message.system {
|
||||
background: rgba(233, 69, 96, 0.1);
|
||||
border-left: 3px solid var(--accent);
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
|||
198
src/lib/stores/events.ts
Normal file
198
src/lib/stores/events.ts
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
// Claude Desktop — Event-Bridge
|
||||
// Empfängt Events vom Tauri-Backend und aktualisiert die Stores
|
||||
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||
import {
|
||||
agents,
|
||||
toolCalls,
|
||||
messages,
|
||||
isProcessing,
|
||||
addMessage,
|
||||
addAgent,
|
||||
updateAgentStatus,
|
||||
addToolCall,
|
||||
completeToolCall,
|
||||
clearAll
|
||||
} from './app';
|
||||
|
||||
// Event-Typen vom Backend
|
||||
interface AgentEvent {
|
||||
id: string;
|
||||
type?: string;
|
||||
task?: string;
|
||||
code?: number;
|
||||
}
|
||||
|
||||
interface ToolEvent {
|
||||
id: string;
|
||||
tool?: string;
|
||||
input?: Record<string, unknown>;
|
||||
output?: string;
|
||||
success?: boolean;
|
||||
}
|
||||
|
||||
interface TextEvent {
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface ResultEvent {
|
||||
cost?: number;
|
||||
tokens?: {
|
||||
input: number;
|
||||
output: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Listener-Handles
|
||||
let listeners: UnlistenFn[] = [];
|
||||
|
||||
// Aktuelle Nachricht (wird während Streaming aufgebaut)
|
||||
let currentResponseText = '';
|
||||
let currentResponseAgentId: string | null = null;
|
||||
|
||||
// Events initialisieren
|
||||
export async function initEventListeners(): Promise<void> {
|
||||
console.log('🎧 Initialisiere Event-Listener...');
|
||||
|
||||
// Aufräumen falls bereits initialisiert
|
||||
await cleanupEventListeners();
|
||||
|
||||
// Bridge bereit
|
||||
listeners.push(
|
||||
await listen('bridge-ready', () => {
|
||||
console.log('✅ Bridge bereit');
|
||||
})
|
||||
);
|
||||
|
||||
// Agent gestartet
|
||||
listeners.push(
|
||||
await listen<AgentEvent>('agent-started', (event) => {
|
||||
const { id, type, task } = event.payload;
|
||||
console.log('🤖 Agent gestartet:', id, type);
|
||||
|
||||
const agentType = mapAgentType(type || 'main');
|
||||
addAgent(agentType, task || 'Verarbeite...');
|
||||
currentResponseAgentId = id;
|
||||
isProcessing.set(true);
|
||||
})
|
||||
);
|
||||
|
||||
// Agent gestoppt
|
||||
listeners.push(
|
||||
await listen<AgentEvent>('agent-stopped', (event) => {
|
||||
const { id } = event.payload;
|
||||
console.log('⏹️ Agent gestoppt:', id);
|
||||
|
||||
updateAgentStatus(id, 'stopped');
|
||||
|
||||
// Falls das der Haupt-Agent war, Antwort finalisieren
|
||||
if (currentResponseAgentId === id && currentResponseText) {
|
||||
addMessage('assistant', currentResponseText, id);
|
||||
currentResponseText = '';
|
||||
currentResponseAgentId = null;
|
||||
}
|
||||
|
||||
// Prüfen ob noch Agents aktiv
|
||||
agents.update((ags) => {
|
||||
const stillActive = ags.some((a) => a.status === 'active');
|
||||
if (!stillActive) {
|
||||
isProcessing.set(false);
|
||||
}
|
||||
return ags;
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Alle Agents gestoppt
|
||||
listeners.push(
|
||||
await listen('all-stopped', () => {
|
||||
console.log('⏹️ Alle Agents gestoppt');
|
||||
agents.update((ags) => ags.map((a) => ({ ...a, status: 'stopped' as const })));
|
||||
isProcessing.set(false);
|
||||
|
||||
if (currentResponseText) {
|
||||
addMessage('assistant', currentResponseText);
|
||||
currentResponseText = '';
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Tool Start
|
||||
listeners.push(
|
||||
await listen<ToolEvent>('tool-start', (event) => {
|
||||
const { id, tool, input } = event.payload;
|
||||
console.log('🔧 Tool Start:', tool);
|
||||
|
||||
// Dem aktiven Haupt-Agent zuordnen
|
||||
agents.update((ags) => {
|
||||
const activeAgent = ags.find((a) => a.status === 'active');
|
||||
if (activeAgent) {
|
||||
addToolCall(activeAgent.id, tool || 'unknown', input || {});
|
||||
}
|
||||
return ags;
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Tool Ende
|
||||
listeners.push(
|
||||
await listen<ToolEvent>('tool-end', (event) => {
|
||||
const { id, success, output } = event.payload;
|
||||
console.log('✅ Tool Ende:', id, success ? 'OK' : 'FEHLER');
|
||||
|
||||
completeToolCall(id, output, !success);
|
||||
})
|
||||
);
|
||||
|
||||
// Text-Streaming
|
||||
listeners.push(
|
||||
await listen<TextEvent>('claude-text', (event) => {
|
||||
const { text } = event.payload;
|
||||
currentResponseText += text;
|
||||
})
|
||||
);
|
||||
|
||||
// Ergebnis (Kosten, Token)
|
||||
listeners.push(
|
||||
await listen<ResultEvent>('claude-result', (event) => {
|
||||
const { cost, tokens } = event.payload;
|
||||
console.log('📊 Ergebnis:', {
|
||||
cost: cost ? `$${cost.toFixed(4)}` : 'unbekannt',
|
||||
tokens
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Agents gestoppt (vom STOPP-Button)
|
||||
listeners.push(
|
||||
await listen('agents-stopped', () => {
|
||||
console.log('🛑 STOPP-Signal empfangen');
|
||||
clearAll();
|
||||
})
|
||||
);
|
||||
|
||||
console.log('✅ Event-Listener initialisiert');
|
||||
}
|
||||
|
||||
// Listener aufräumen
|
||||
export async function cleanupEventListeners(): Promise<void> {
|
||||
for (const unlisten of listeners) {
|
||||
unlisten();
|
||||
}
|
||||
listeners = [];
|
||||
}
|
||||
|
||||
// Agent-Typ mappen
|
||||
function mapAgentType(type: string): 'main' | 'explore' | 'plan' | 'bash' {
|
||||
const typeMap: Record<string, 'main' | 'explore' | 'plan' | 'bash'> = {
|
||||
main: 'main',
|
||||
'Main Agent': 'main',
|
||||
explore: 'explore',
|
||||
Explore: 'explore',
|
||||
plan: 'plan',
|
||||
Plan: 'plan',
|
||||
bash: 'bash',
|
||||
Bash: 'bash'
|
||||
};
|
||||
return typeMap[type] || 'main';
|
||||
}
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
// Stores re-export
|
||||
export * from './app';
|
||||
export * from './events';
|
||||
|
|
|
|||
|
|
@ -1,12 +1,27 @@
|
|||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { isProcessing, agentCount } from '$lib/stores/app';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { isProcessing, agentCount, initEventListeners, cleanupEventListeners } from '$lib/stores';
|
||||
import StopButton from '$lib/components/StopButton.svelte';
|
||||
|
||||
// Events beim Laden initialisieren
|
||||
onMount(async () => {
|
||||
await initEventListeners();
|
||||
});
|
||||
|
||||
onDestroy(async () => {
|
||||
await cleanupEventListeners();
|
||||
});
|
||||
|
||||
// STOPP-Funktion
|
||||
async function handleStop() {
|
||||
console.log('STOPP gedrückt — breche alle Agents ab');
|
||||
// TODO: Tauri-Command zum Abbrechen aufrufen
|
||||
try {
|
||||
await invoke('stop_all_agents');
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Stoppen:', err);
|
||||
}
|
||||
$isProcessing = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue