[appimage] Phase 3 komplett: Bridge-Daemon + Unix Socket IPC
Some checks failed
Build AppImage / build (push) Has been cancelled

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 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-21 14:21:25 +02:00
parent d29bbd7bfd
commit e36209690e
7 changed files with 433 additions and 51 deletions

View file

@ -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`)

View file

@ -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) |
---

View file

@ -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) => {
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');
}
}
if (IS_DAEMON) {
// ---- UDS-Daemon-Modus ----
// Alte Socket-Datei aufräumen
try { if (existsSync(SOCKET_PATH)) unlinkSync(SOCKET_PATH); } catch {}
// PID-File schreiben
writeFileSync(PID_PATH, String(process.pid));
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', () => {
rl.on('close', () => {
process.stderr.write('stdin geschlossen\n');
});
});
}
process.on('SIGTERM', () => { clearInterval(keepAlive); process.exit(0); });
process.on('SIGINT', () => { clearInterval(keepAlive); process.exit(0); });
process.on('SIGTERM', () => { clearInterval(keepAlive); cleanupDaemon(); process.exit(0); });
process.on('SIGINT', () => { clearInterval(keepAlive); cleanupDaemon(); process.exit(0); });
process.on('exit', () => { cleanupDaemon(); });
// Bereit
sendEvent('ready', { version: '1.1.0', pid: process.pid, model: currentModel, availableModels: AVAILABLE_MODELS });
// 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 });
}

1
src-tauri/Cargo.lock generated
View file

@ -482,6 +482,7 @@ dependencies = [
"base64 0.22.1",
"chrono",
"futures-util",
"libc",
"mysql_async",
"reqwest 0.12.28",
"rusqlite",

View file

@ -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"

View file

@ -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<String>,
}
/// 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<u32>,
},
}
/// Globaler State für die Bridge
pub struct ClaudeState {
pub bridge_process: Option<std::process::Child>,
pub bridge_stdin: Option<std::process::ChildStdin>,
pub connection: Option<BridgeConnection>,
pub request_counter: u64,
pub agents: Vec<AgentStatus>,
}
@ -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<std::path::PathBuf, String> {
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<u32> {
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<u32, String> {
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<u32>) -> 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::<Arc<Mutex<ClaudeState>>>();
{
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::<Arc<Mutex<ClaudeState>>>().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::<BridgeMessage>(&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::<Arc<Mutex<ClaudeState>>>();
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::<Arc<Mutex<ClaudeState>>>();
{
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())?;
state.write_line(&msg.to_string())?;
Ok(request_id)
} else {
Err("Bridge nicht gestartet".to_string())
}
}
// ============ Tauri Commands ============
@ -388,7 +600,7 @@ pub async fn send_message(app: AppHandle, message: String) -> Result<String, Str
let needs_start = {
let state = app.state::<Arc<Mutex<ClaudeState>>>();
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<String, String>
let needs_start = {
let state = app.state::<Arc<Mutex<ClaudeState>>>();
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<String, Stri
let needs_start = {
let state = app.state::<Arc<Mutex<ClaudeState>>>();
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<StickyContextInfo, St
let needs_start = {
let state = app.state::<Arc<Mutex<ClaudeState>>>();
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<StickyContextInfo, St
Ok(info)
}
/// Bridge-Verbindungsstatus abfragen
#[derive(Debug, Clone, serde::Serialize)]
pub struct BridgeStatus {
pub connected: bool,
pub mode: String, // "uds" | "stdio" | "disconnected"
pub daemon_pid: Option<u32>,
pub socket_path: String,
}
#[tauri::command]
pub async fn get_bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
let state = app.state::<Arc<Mutex<ClaudeState>>>();
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<String, String> {
// Verbindung trennen
{
let state = app.state::<Arc<Mutex<ClaudeState>>>();
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())
}

View file

@ -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 {