claude-desktop/src-tauri/src/claude.rs
Eddy c882e23445 Fix: Bridge-Prozess im State speichern (verhindert Drop/Kill)
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>
2026-04-13 21:10:38 +02:00

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())
}