From e36209690e34440fc6b68f65cf9148ffe6623684 Mon Sep 17 00:00:00 2001 From: Eddy Date: Tue, 21 Apr 2026 14:21:25 +0200 Subject: [PATCH] [appimage] Phase 3 komplett: Bridge-Daemon + Unix Socket IPC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bridge überlebt jetzt App-Neustarts als eigenständiger Daemon-Prozess. Kommunikation über Unix Domain Socket statt stdio — async, Auto-Reconnect, PID-Tracking. Fallback auf stdio-Modus wenn UDS nicht verfügbar. Neue Commands: get_bridge_status, stop_bridge_daemon. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 11 +- ROADMAP.md | 6 +- scripts/claude-bridge.js | 139 ++++++++++++++--- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/claude.rs | 320 +++++++++++++++++++++++++++++++++++---- src-tauri/src/lib.rs | 6 +- 7 files changed, 433 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94a83ef..71e54c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,18 @@ Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/). --- -## [Unreleased] - 2026-04-21 +## [Unreleased] - 2026-04-22 ### Hinzugefügt +- **Bridge-Daemon (Phase 3)**: Bridge läuft als eigenständiger Daemon-Prozess, überlebt App-Neustarts — kein Cold-Start mehr (`claude-bridge.js --socket`, `claude.rs`) +- **Unix Socket IPC (Phase 3)**: Kommunikation über Unix Domain Socket statt stdio — async, kein Block, Auto-Reconnect bei Verbindungsverlust (`claude.rs`, `claude-bridge.js`) +- **Bridge-Status API**: `get_bridge_status` Command — zeigt Verbindungsmodus (UDS/stdio), Daemon-PID, Socket-Pfad +- **Daemon-Steuerung**: `stop_bridge_daemon` Command zum expliziten Stoppen des Daemon-Prozesses +- **Modus-Indikator**: Badge im ChatPanel zeigt aktuellen Agent-Modus (Handlanger/Experten/Auto) mit Verarbeitungsphase +- **Plan-Erkennung**: Claude-Antworten mit Plänen werden automatisch als Slides an das Präsentationsfenster gesendet (`planPresentation.ts`) +- **Session-Projekt-Filter**: Sessions werden nach aktivem Projekt/Workspace gefiltert (`db.rs`, `session.rs`) +- **Weibliche TTS-Stimme**: Kerstin als Standard-Stimme, 5 deutsche Stimmen wählbar (`voice.rs`) +- **UTF-8 Crash Fix**: Kein Panic mehr bei Multi-Byte-Zeichen in DB-Abfragen (`db.rs`, `knowledge.rs`) - **Guard-Rails UI (Live)**: 3-Tab-Ansicht (Live-Feed/Regeln/Blockiert), Risiko-Statistik-Leiste, Ein-Klick-Freigabe bei Bestätigungsbedarf, guard-check Events vom Backend (`GuardRailsPanel.svelte`, `guard.rs`) - **D-Bus Desktop-Aktionen**: 10 vordefinierte Aktionen (Dolphin, Kate, Konsole, Firefox, Notify, Lock Screen), Aktionen-Grid im ProgramsPanel, CLI/GUI-Unterscheidung (`programs.rs`, `ProgramsPanel.svelte`) - **Screenshot-Analyse**: Bildschirmbereich oder Vollbild capturen via spectacle/scrot/gnome-screenshot, Vorschau im Panel, "An Claude senden" Button (`programs.rs`, `ProgramsPanel.svelte`) diff --git a/ROADMAP.md b/ROADMAP.md index cf52cf9..8714a20 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # Claude Desktop — Roadmap -Stand: 21.04.2026 +Stand: 22.04.2026 --- @@ -49,8 +49,8 @@ Alles aus Phase 1-16 ist implementiert und funktionsfaehig: | ✅ Auto-Retry (Netzwerk) | `claude-bridge.js` | 3x Backoff bei Rate-Limit/5xx | | ✅ Bridge Heartbeat | `claude-bridge.js` | 30s Pulse an Rust | | ✅ FIFO Message Queue | `ChatPanel.svelte` | Mehrere Nachrichten queuen | -| ⬜ Bridge-Daemon | `claude.rs`, `claude-bridge.js` | Bridge ueberlebt App-Neustart | -| ⬜ Unix Socket IPC | `claude.rs`, `claude-bridge.js` | stdio → UDS (async, kein Block) | +| ✅ Bridge-Daemon | `claude.rs`, `claude-bridge.js` | Bridge ueberlebt App-Neustart (--socket Flag) | +| ✅ Unix Socket IPC | `claude.rs`, `claude-bridge.js` | stdio → UDS (async, Reconnect, PID-Tracking) | --- diff --git a/scripts/claude-bridge.js b/scripts/claude-bridge.js index 37705b7..4d46ab7 100644 --- a/scripts/claude-bridge.js +++ b/scripts/claude-bridge.js @@ -4,16 +4,32 @@ // Nutzt @anthropic-ai/claude-agent-sdk (query-Funktion) // OAuth-Auth funktioniert automatisch (Claude Max Abo) // Kein CLI-Spawn, kein Overhead — direkte SDK-Aufrufe +// +// Modi: +// stdio (Default): node claude-bridge.js +// UDS-Daemon: node claude-bridge.js --socket /tmp/claude-bridge.sock import { query } from '@anthropic-ai/claude-agent-sdk'; import { createInterface } from 'node:readline'; import { randomUUID } from 'node:crypto'; +import { createServer } from 'node:net'; +import { writeFileSync, unlinkSync, existsSync } from 'node:fs'; -// Prozess am Leben halten + Heartbeat an Rust alle 30s +// ============ IPC-Modus erkennen ============ + +const socketArg = process.argv.indexOf('--socket'); +const SOCKET_PATH = socketArg !== -1 ? process.argv[socketArg + 1] : null; +const PID_PATH = SOCKET_PATH ? SOCKET_PATH.replace('.sock', '.pid') : null; +const IS_DAEMON = !!SOCKET_PATH; + +// Aktive UDS-Clients (bei Daemon-Modus) +let udsClients = new Set(); + +// Prozess am Leben halten + Heartbeat alle 30s const keepAlive = setInterval(() => { sendEvent('heartbeat', { ts: Date.now(), uptime: process.uptime() }); }, 30000); -process.stdin.resume(); +if (!IS_DAEMON) process.stdin.resume(); // ============ State ============ @@ -203,7 +219,22 @@ function getSubagentType(toolName, input) { // ============ Kommunikation mit Tauri ============ function sendToTauri(msg) { - process.stdout.write(JSON.stringify(msg) + '\n'); + const line = JSON.stringify(msg) + '\n'; + + if (IS_DAEMON) { + // UDS-Modus: An alle verbundenen Clients senden + for (const client of udsClients) { + try { + client.write(line); + } catch (err) { + process.stderr.write(`UDS-Client Schreibfehler: ${err.message}\n`); + udsClients.delete(client); + } + } + } else { + // stdio-Modus: An stdout (wie bisher) + process.stdout.write(line); + } } function sendEvent(event, payload = {}) { @@ -828,22 +859,94 @@ function handleCommand(msg) { // ============ Main ============ -const rl = createInterface({ input: process.stdin }); -rl.on('line', (line) => { - if (!line.trim()) return; - try { - handleCommand(JSON.parse(line)); - } catch (err) { - process.stderr.write(`Ungültige Eingabe: ${err.message}\n`); +function cleanupDaemon() { + if (IS_DAEMON) { + try { if (existsSync(SOCKET_PATH)) unlinkSync(SOCKET_PATH); } catch {} + try { if (existsSync(PID_PATH)) unlinkSync(PID_PATH); } catch {} + process.stderr.write('🔌 Daemon aufgeräumt\n'); } -}); +} -rl.on('close', () => { - process.stderr.write('stdin geschlossen\n'); -}); +if (IS_DAEMON) { + // ---- UDS-Daemon-Modus ---- + // Alte Socket-Datei aufräumen + try { if (existsSync(SOCKET_PATH)) unlinkSync(SOCKET_PATH); } catch {} -process.on('SIGTERM', () => { clearInterval(keepAlive); process.exit(0); }); -process.on('SIGINT', () => { clearInterval(keepAlive); process.exit(0); }); + // PID-File schreiben + writeFileSync(PID_PATH, String(process.pid)); -// Bereit -sendEvent('ready', { version: '1.1.0', pid: process.pid, model: currentModel, availableModels: AVAILABLE_MODELS }); + const udsServer = createServer((client) => { + process.stderr.write(`🔌 UDS-Client verbunden (${udsClients.size + 1} aktiv)\n`); + udsClients.add(client); + + // JSON-Lines Protokoll: jede Zeile = ein Befehl + let buffer = ''; + client.on('data', (data) => { + buffer += data.toString(); + let idx; + while ((idx = buffer.indexOf('\n')) !== -1) { + const line = buffer.slice(0, idx).trim(); + buffer = buffer.slice(idx + 1); + if (!line) continue; + try { + handleCommand(JSON.parse(line)); + } catch (err) { + process.stderr.write(`UDS Ungültige Eingabe: ${err.message}\n`); + } + } + }); + + client.on('end', () => { + udsClients.delete(client); + process.stderr.write(`🔌 UDS-Client getrennt (${udsClients.size} aktiv)\n`); + }); + + client.on('error', (err) => { + udsClients.delete(client); + process.stderr.write(`UDS-Client Fehler: ${err.message}\n`); + }); + + // Neuem Client sofort den aktuellen Status senden + try { + client.write(JSON.stringify({ + type: 'event', event: 'ready', + payload: { version: '1.2.0', pid: process.pid, model: currentModel, availableModels: AVAILABLE_MODELS, daemon: true } + }) + '\n'); + } catch {} + }); + + udsServer.listen(SOCKET_PATH, () => { + process.stderr.write(`🔌 Bridge-Daemon lauscht auf ${SOCKET_PATH} (PID: ${process.pid})\n`); + }); + + udsServer.on('error', (err) => { + process.stderr.write(`UDS-Server Fehler: ${err.message}\n`); + cleanupDaemon(); + process.exit(1); + }); + +} else { + // ---- stdio-Modus (Kompatibilität) ---- + const rl = createInterface({ input: process.stdin }); + rl.on('line', (line) => { + if (!line.trim()) return; + try { + handleCommand(JSON.parse(line)); + } catch (err) { + process.stderr.write(`Ungültige Eingabe: ${err.message}\n`); + } + }); + + rl.on('close', () => { + process.stderr.write('stdin geschlossen\n'); + }); +} + +process.on('SIGTERM', () => { clearInterval(keepAlive); cleanupDaemon(); process.exit(0); }); +process.on('SIGINT', () => { clearInterval(keepAlive); cleanupDaemon(); process.exit(0); }); +process.on('exit', () => { cleanupDaemon(); }); + +// Bereit-Signal (im stdio-Modus sofort senden, im Daemon-Modus pro Client bei Connect) +if (!IS_DAEMON) { + sendEvent('ready', { version: '1.2.0', pid: process.pid, model: currentModel, availableModels: AVAILABLE_MODELS }); +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6c38952..e8e09a6 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -482,6 +482,7 @@ dependencies = [ "base64 0.22.1", "chrono", "futures-util", + "libc", "mysql_async", "reqwest 0.12.28", "rusqlite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c309f1d..72c5336 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -29,6 +29,7 @@ futures-util = "0.3" sha2 = "0.10" tauri-plugin-dialog = "2.7.0" tauri-plugin-global-shortcut = "2.3.1" +libc = "0.2" [target.'cfg(target_os = "linux")'.dependencies] webkit2gtk = "2.0" diff --git a/src-tauri/src/claude.rs b/src-tauri/src/claude.rs index d12a090..780a7bd 100644 --- a/src-tauri/src/claude.rs +++ b/src-tauri/src/claude.rs @@ -1,5 +1,9 @@ // Claude Desktop — Claude SDK Integration -// Kommunikation mit Claude Code via Node.js Child-Process +// Kommunikation mit Claude Code via Node.js Child-Process oder Unix Domain Socket +// +// Modi: +// 1. UDS-Daemon: Bridge läuft als eigenständiger Prozess, App verbindet sich über Socket +// 2. stdio (Fallback): Bridge als Child-Process mit stdin/stdout (wie bisher) use serde::{Deserialize, Serialize}; use std::io::{BufRead, BufReader, Write}; @@ -7,9 +11,16 @@ use std::process::{Command, Stdio}; use std::sync::{Arc, Mutex}; use tauri::{AppHandle, Emitter, Manager}; +#[cfg(unix)] +use std::os::unix::net::UnixStream; + use crate::db; use crate::knowledge; +/// Standard-Pfade für UDS-Daemon +const SOCKET_PATH: &str = "/tmp/claude-bridge.sock"; +const PID_PATH: &str = "/tmp/claude-bridge.pid"; + /// Status eines Agents #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentStatus { @@ -53,10 +64,26 @@ struct BridgeMessage { error: Option, } +/// IPC-Modus der Bridge-Verbindung +#[derive(Debug)] +pub enum BridgeConnection { + /// stdio: Bridge ist ein Child-Process + Stdio { + #[allow(dead_code)] // process-Handle muss am Leben bleiben! + process: std::process::Child, + stdin: std::process::ChildStdin, + }, + /// UDS: Bridge ist ein Daemon, Verbindung über Unix Socket + #[cfg(unix)] + Uds { + stream: UnixStream, + daemon_pid: Option, + }, +} + /// Globaler State für die Bridge pub struct ClaudeState { - pub bridge_process: Option, - pub bridge_stdin: Option, + pub connection: Option, pub request_counter: u64, pub agents: Vec, } @@ -64,20 +91,40 @@ pub struct ClaudeState { impl Default for ClaudeState { fn default() -> Self { Self { - bridge_process: None, - bridge_stdin: None, + connection: None, request_counter: 0, agents: vec![], } } } -/// Bridge starten -pub fn start_bridge(app: &AppHandle) -> Result<(), String> { - // Smart Hints v2: Session-Topic zurücksetzen bei neuer Bridge/Session - knowledge::reset_session_topic(); +impl ClaudeState { + /// Prüft ob eine aktive Verbindung besteht + pub fn is_connected(&self) -> bool { + self.connection.is_some() + } - // Script-Pfad ermitteln + /// Schreibt eine Zeile an die Bridge (JSON-Line) + pub fn write_line(&mut self, line: &str) -> Result<(), String> { + match &mut self.connection { + Some(BridgeConnection::Stdio { stdin, .. }) => { + writeln!(stdin, "{}", line).map_err(|e| e.to_string())?; + stdin.flush().map_err(|e| e.to_string())?; + Ok(()) + } + #[cfg(unix)] + Some(BridgeConnection::Uds { stream, .. }) => { + writeln!(stream, "{}", line).map_err(|e| e.to_string())?; + stream.flush().map_err(|e| e.to_string())?; + Ok(()) + } + None => Err("Bridge nicht verbunden".to_string()), + } + } +} + +/// Script-Pfad der Bridge ermitteln +fn find_bridge_script() -> Result { let exe_dir = std::env::current_exe() .map_err(|e| e.to_string())? .parent() @@ -97,22 +144,190 @@ pub fn start_bridge(app: &AppHandle) -> Result<(), String> { std::env::current_dir().unwrap_or_default().join("scripts/claude-bridge.js"), ]; - let script_path = candidates.iter() + candidates.iter() .find(|p| p.exists()) .cloned() - .ok_or_else(|| format!("claude-bridge.js nicht gefunden. Gesucht in: {:?}", candidates))?; + .ok_or_else(|| format!("claude-bridge.js nicht gefunden. Gesucht in: {:?}", candidates)) +} - println!("🔌 Starte Claude Bridge: {:?}", script_path); +/// Prüft ob ein Daemon-Prozess noch lebt +#[cfg(unix)] +fn is_daemon_alive() -> Option { + let pid_path = std::path::Path::new(PID_PATH); + if !pid_path.exists() { return None; } - // Arbeitsverzeichnis = Projektroot (wo node_modules liegt) + let pid_str = std::fs::read_to_string(pid_path).ok()?; + let pid: u32 = pid_str.trim().parse().ok()?; + + // Signal 0 prüft ob Prozess existiert, ohne ihn zu beeinflussen + let alive = unsafe { libc::kill(pid as i32, 0) == 0 }; + if alive { Some(pid) } else { None } +} + +/// Startet Bridge-Daemon als eigenständigen Prozess (überlebt App-Neustart) +#[cfg(unix)] +fn start_daemon(script_path: &std::path::Path) -> Result { let project_dir = script_path.parent() .and_then(|p| p.parent()) .unwrap_or_else(|| std::path::Path::new(".")); + println!("🔌 Starte Bridge-Daemon: {:?} --socket {}", script_path, SOCKET_PATH); + + let child = Command::new("node") + .arg(script_path) + .arg("--socket") + .arg(SOCKET_PATH) + .current_dir(project_dir) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|e| format!("Bridge-Daemon konnte nicht gestartet werden: {}", e))?; + + let pid = child.id(); + + // Stderr in separatem Thread lesen (Daemon-Logs) + if let Some(stderr) = child.stderr { + std::thread::spawn(move || { + let reader = BufReader::new(stderr); + for line in reader.lines().map_while(Result::ok) { + println!("🔌 Daemon: {}", line); + } + }); + } + + // Kurz warten bis Socket-Datei erstellt wird + for _ in 0..20 { + if std::path::Path::new(SOCKET_PATH).exists() { + println!("✅ Bridge-Daemon gestartet (PID: {})", pid); + return Ok(pid); + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } + + Err(format!("Bridge-Daemon gestartet (PID: {}), aber Socket {} nicht erstellt", pid, SOCKET_PATH)) +} + +/// Verbindet sich mit dem UDS-Daemon und startet Reader-Thread +#[cfg(unix)] +fn connect_uds(app: &AppHandle, daemon_pid: Option) -> Result<(), String> { + let stream = UnixStream::connect(SOCKET_PATH) + .map_err(|e| format!("UDS-Verbindung fehlgeschlagen: {}", e))?; + + // Reader-Stream klonen für den Lese-Thread + let reader_stream = stream.try_clone() + .map_err(|e| format!("UDS-Stream klonen fehlgeschlagen: {}", e))?; + + // Verbindung speichern + let state = app.state::>>(); + { + let mut state = state.lock().unwrap(); + state.connection = Some(BridgeConnection::Uds { + stream, + daemon_pid, + }); + } + + // Reader-Thread: JSON-Lines vom Socket lesen + let app_handle = app.clone(); + let state_for_reconnect = app.state::>>().inner().clone(); + std::thread::spawn(move || { + let reader = BufReader::new(reader_stream); + for line in reader.lines().map_while(Result::ok) { + if let Ok(msg) = serde_json::from_str::(&line) { + handle_bridge_message(&app_handle, msg); + } + } + println!("⚠️ UDS-Verbindung getrennt — versuche Reconnect..."); + + // Verbindung als geschlossen markieren + { + let mut state = state_for_reconnect.lock().unwrap(); + state.connection = None; + } + + // Automatischer Reconnect (3 Versuche) + for attempt in 1..=3 { + std::thread::sleep(std::time::Duration::from_secs(attempt)); + if std::path::Path::new(SOCKET_PATH).exists() { + println!("🔄 UDS Reconnect Versuch {}/3...", attempt); + match connect_uds(&app_handle, daemon_pid) { + Ok(()) => { + println!("✅ UDS Reconnect erfolgreich"); + let _ = app_handle.emit("bridge-ready", ()); + return; + } + Err(e) => println!("⚠️ UDS Reconnect fehlgeschlagen: {}", e), + } + } + } + println!("❌ UDS Reconnect endgültig fehlgeschlagen"); + let _ = app_handle.emit("bridge-disconnected", ()); + }); + + println!("✅ UDS-Verbindung hergestellt"); + Ok(()) +} + +/// Bridge starten — versucht erst UDS-Daemon, dann stdio-Fallback +pub fn start_bridge(app: &AppHandle) -> Result<(), String> { + // Smart Hints v2: Session-Topic zurücksetzen bei neuer Bridge/Session + knowledge::reset_session_topic(); + + // Bereits verbunden? + { + let state = app.state::>>(); + let state_guard = state.lock().unwrap(); + if state_guard.is_connected() { + println!("🔌 Bridge bereits verbunden"); + return Ok(()); + } + } + + let script_path = find_bridge_script()?; + + // ---- UDS-Daemon-Modus (bevorzugt) ---- + #[cfg(unix)] + { + // 1. Läuft schon ein Daemon? + if let Some(pid) = is_daemon_alive() { + println!("🔌 Existierender Bridge-Daemon gefunden (PID: {})", pid); + if let Ok(()) = connect_uds(app, Some(pid)) { + return Ok(()); + } + println!("⚠️ Verbindung zu bestehendem Daemon fehlgeschlagen, starte neu..."); + // Alten Daemon killen + unsafe { libc::kill(pid as i32, libc::SIGTERM); } + std::thread::sleep(std::time::Duration::from_millis(500)); + } + + // 2. Neuen Daemon starten + match start_daemon(&script_path) { + Ok(pid) => { + match connect_uds(app, Some(pid)) { + Ok(()) => return Ok(()), + Err(e) => println!("⚠️ UDS-Verbindung nach Daemon-Start fehlgeschlagen: {} — Fallback auf stdio", e), + } + } + Err(e) => println!("⚠️ Daemon-Start fehlgeschlagen: {} — Fallback auf stdio", e), + } + } + + // ---- stdio-Fallback (Kompatibilität) ---- + start_bridge_stdio(app, &script_path) +} + +/// Bridge im stdio-Modus starten (Child-Process, wie bisher) +fn start_bridge_stdio(app: &AppHandle, script_path: &std::path::Path) -> Result<(), String> { + let project_dir = script_path.parent() + .and_then(|p| p.parent()) + .unwrap_or_else(|| std::path::Path::new(".")); + + println!("🔌 Starte Claude Bridge (stdio): {:?}", script_path); println!("📂 Bridge Arbeitsverzeichnis: {:?}", project_dir); let mut child = Command::new("node") - .arg(&script_path) + .arg(script_path) .current_dir(project_dir) .stdin(Stdio::piped()) .stdout(Stdio::piped()) @@ -128,8 +343,10 @@ pub fn start_bridge(app: &AppHandle) -> Result<(), String> { let state = app.state::>>(); { let mut state = state.lock().unwrap(); - state.bridge_process = Some(child); - state.bridge_stdin = Some(stdin); + state.connection = Some(BridgeConnection::Stdio { + process: child, + stdin, + }); } // Stderr in separatem Thread lesen und loggen @@ -368,13 +585,8 @@ fn send_to_bridge_full( }), }; - 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()) - } + state.write_line(&msg.to_string())?; + Ok(request_id) } // ============ Tauri Commands ============ @@ -388,7 +600,7 @@ pub async fn send_message(app: AppHandle, message: String) -> Result>>(); let state_guard = state.lock().unwrap(); - state_guard.bridge_stdin.is_none() + !state_guard.is_connected() }; if needs_start { @@ -549,7 +761,7 @@ pub async fn set_model(app: AppHandle, model: String) -> Result let needs_start = { let state = app.state::>>(); let state_guard = state.lock().unwrap(); - state_guard.bridge_stdin.is_none() + !state_guard.is_connected() }; if needs_start { @@ -618,7 +830,7 @@ pub async fn set_agent_mode(app: AppHandle, mode: String) -> Result>>(); let state_guard = state.lock().unwrap(); - state_guard.bridge_stdin.is_none() + !state_guard.is_connected() }; if needs_start { @@ -743,7 +955,7 @@ pub async fn init_sticky_context(app: AppHandle) -> Result>>(); let state_guard = state.lock().unwrap(); - state_guard.bridge_stdin.is_none() + !state_guard.is_connected() }; if needs_start { @@ -762,3 +974,55 @@ pub async fn init_sticky_context(app: AppHandle) -> Result, + pub socket_path: String, +} + +#[tauri::command] +pub async fn get_bridge_status(app: AppHandle) -> Result { + let state = app.state::>>(); + let state = state.lock().unwrap(); + + let (connected, mode, daemon_pid) = match &state.connection { + Some(BridgeConnection::Stdio { .. }) => (true, "stdio".to_string(), None), + #[cfg(unix)] + Some(BridgeConnection::Uds { daemon_pid, .. }) => (true, "uds".to_string(), *daemon_pid), + None => (false, "disconnected".to_string(), None), + }; + + Ok(BridgeStatus { + connected, + mode, + daemon_pid, + socket_path: SOCKET_PATH.to_string(), + }) +} + +/// Bridge-Daemon explizit stoppen (z.B. für Neustart oder Debugging) +#[tauri::command] +pub async fn stop_bridge_daemon(app: AppHandle) -> Result { + // Verbindung trennen + { + let state = app.state::>>(); + let mut state = state.lock().unwrap(); + state.connection = None; + } + + // Daemon-Prozess killen + #[cfg(unix)] + { + if let Some(pid) = is_daemon_alive() { + unsafe { libc::kill(pid as i32, libc::SIGTERM); } + println!("🔌 Bridge-Daemon (PID: {}) wird gestoppt", pid); + return Ok(format!("Daemon PID {} gestoppt", pid)); + } + } + + Ok("Kein aktiver Daemon gefunden".to_string()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 61f530d..3d3829c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -54,6 +54,8 @@ pub fn run() { claude::set_agent_mode, claude::get_agent_mode, claude::init_sticky_context, + claude::get_bridge_status, + claude::stop_bridge_daemon, // Gedächtnis-System memory::load_memory, memory::get_sticky_memory_entries, @@ -325,7 +327,9 @@ pub fn run() { // Lock-Datei bei App-Beendigung aufräumen if let tauri::RunEvent::Exit = event { update::remove_lock_file(); - println!("🔒 Lock-Datei aufgeräumt, App beendet."); + // Bridge-Daemon am Leben lassen! Er überlebt App-Neustarts. + // Nur die UDS-Verbindung wird geschlossen (Drop). + println!("🔒 Lock-Datei aufgeräumt, App beendet. Bridge-Daemon läuft weiter."); } // Bei Fenster-Schließen: Lock entfernen falls App komplett beendet wird if let tauri::RunEvent::WindowEvent {