Child-Objekt wurde am Ende von start_bridge() gedroppt — das schloss die Pipes und beendete den Node-Prozess. Jetzt wird child im ClaudeState gespeichert und lebt solange die App läuft. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
325 lines
12 KiB
Rust
325 lines
12 KiB
Rust
// Claude Desktop — Claude SDK Integration
|
|
// Kommunikation mit Claude Code via Node.js Child-Process
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use std::io::{BufRead, BufReader, Write};
|
|
use std::process::{Command, Stdio};
|
|
use std::sync::{Arc, Mutex};
|
|
use tauri::{AppHandle, Emitter, Manager};
|
|
|
|
use crate::db;
|
|
|
|
/// Status eines Agents
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AgentStatus {
|
|
pub id: String,
|
|
pub agent_type: String,
|
|
pub status: String,
|
|
pub task: String,
|
|
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_process: Option<std::process::Child>,
|
|
pub bridge_stdin: Option<std::process::ChildStdin>,
|
|
pub request_counter: u64,
|
|
pub agents: Vec<AgentStatus>,
|
|
}
|
|
|
|
impl Default for ClaudeState {
|
|
fn default() -> Self {
|
|
Self {
|
|
bridge_process: None,
|
|
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();
|
|
|
|
// Script in mehreren Pfaden suchen
|
|
let candidates = vec![
|
|
exe_dir.join("scripts").join("claude-bridge.js"),
|
|
// Entwicklung: relativ zum Cargo-Manifest
|
|
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../scripts/claude-bridge.js"),
|
|
// Fallback: CWD
|
|
std::env::current_dir().unwrap_or_default().join("scripts/claude-bridge.js"),
|
|
];
|
|
|
|
let script_path = candidates.iter()
|
|
.find(|p| p.exists())
|
|
.cloned()
|
|
.ok_or_else(|| format!("claude-bridge.js nicht gefunden. Gesucht in: {:?}", candidates))?;
|
|
|
|
println!("🔌 Starte Claude Bridge: {:?}", script_path);
|
|
|
|
// Arbeitsverzeichnis = Projektroot (wo node_modules liegt)
|
|
let project_dir = script_path.parent()
|
|
.and_then(|p| p.parent())
|
|
.unwrap_or_else(|| std::path::Path::new("."));
|
|
|
|
println!("📂 Bridge Arbeitsverzeichnis: {:?}", project_dir);
|
|
|
|
let mut child = Command::new("node")
|
|
.arg(&script_path)
|
|
.current_dir(project_dir)
|
|
.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")?;
|
|
let stderr = child.stderr.take();
|
|
|
|
// State speichern — child MUSS am Leben bleiben!
|
|
let state = app.state::<Arc<Mutex<ClaudeState>>>();
|
|
{
|
|
let mut state = state.lock().unwrap();
|
|
state.bridge_process = Some(child);
|
|
state.bridge_stdin = Some(stdin);
|
|
}
|
|
|
|
// Stderr in separatem Thread lesen und loggen
|
|
if let Some(stderr) = stderr {
|
|
std::thread::spawn(move || {
|
|
let reader = BufReader::new(stderr);
|
|
for line in reader.lines().map_while(Result::ok) {
|
|
println!("🔌 Bridge stderr: {}", line);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 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" => {
|
|
// Session-ID aus Result extrahieren und in DB speichern
|
|
if let Some(sid) = payload.get("session_id").and_then(|v| v.as_str()) {
|
|
if let Some(db_state) = app.try_state::<Arc<Mutex<db::Database>>>() {
|
|
let db_lock = db_state.lock().unwrap();
|
|
if let Ok(Some(active_id)) = db_lock.get_setting("active_session_id") {
|
|
if !active_id.is_empty() {
|
|
if let Ok(Some(mut session)) = db_lock.get_session(&active_id) {
|
|
session.claude_session_id = Some(sid.to_string());
|
|
session.message_count += 1;
|
|
if let Some(cost) = payload.get("cost").and_then(|v| v.as_f64()) {
|
|
session.cost_usd += cost;
|
|
}
|
|
if let Some(tin) = payload.get("tokens").and_then(|t| t.get("input")).and_then(|v| v.as_i64()) {
|
|
session.token_input += tin;
|
|
}
|
|
if let Some(tout) = payload.get("tokens").and_then(|t| t.get("output")).and_then(|v| v.as_i64()) {
|
|
session.token_output += tout;
|
|
}
|
|
let _ = db_lock.update_session(&session);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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> {
|
|
println!("📨 Nachricht empfangen: {}", &message[..message.len().min(50)]);
|
|
|
|
// 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()
|
|
};
|
|
|
|
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
|
|
#[tauri::command]
|
|
pub async fn stop_all_agents(app: AppHandle) -> Result<(), String> {
|
|
println!("⏹️ STOPP: Alle Agents werden gestoppt");
|
|
|
|
let _ = send_to_bridge(&app, "stop", "");
|
|
app.emit("agents-stopped", ()).map_err(|e| e.to_string())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Status aller Agents abrufen
|
|
#[tauri::command]
|
|
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())
|
|
}
|