Voice (Phase 10): - voice.rs: OpenAI Whisper (STT) + TTS Backend - ChatPanel: Mikrofon-Button, VAD (Pause 1.5s), Live-Pegel - SettingsPanel: OpenAI-Key Konfiguration Phase 9 Nacharbeiten: - Auto-Extract vor Compacting (Entscheidungen/TODOs/Insights) - get_tool_hints() - relevante KB-Eintraege bei Tool-Start - activeKnowledgeHints Store, Anzeige im KnowledgePanel Tech-Schulden: - Dead-Code in memory.rs entfernt (MemorySystem struct) - cargo-check Warnings behoben Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
648 lines
22 KiB
Rust
648 lines
22 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>,
|
||
#[allow(dead_code)]
|
||
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> {
|
||
send_to_bridge_full(app, command, message, None, None)
|
||
}
|
||
|
||
/// Befehl an Bridge senden mit Context und Resume-Session-ID
|
||
fn send_to_bridge_full(
|
||
app: &AppHandle,
|
||
command: &str,
|
||
message: &str,
|
||
context: Option<String>,
|
||
resume_session_id: Option<String>,
|
||
) -> 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);
|
||
|
||
// Je nach Command unterschiedliche Payload-Struktur
|
||
let msg = match command {
|
||
"set-model" => serde_json::json!({
|
||
"command": command,
|
||
"id": request_id,
|
||
"model": message
|
||
}),
|
||
"set-mode" => serde_json::json!({
|
||
"command": command,
|
||
"id": request_id,
|
||
"mode": message
|
||
}),
|
||
"message" => {
|
||
let mut payload = serde_json::json!({
|
||
"command": command,
|
||
"id": request_id,
|
||
"message": message
|
||
});
|
||
// Context hinzufügen wenn vorhanden
|
||
if let Some(ctx) = context {
|
||
if !ctx.is_empty() {
|
||
payload["context"] = serde_json::Value::String(ctx);
|
||
}
|
||
}
|
||
// Resume-Session-ID hinzufügen wenn vorhanden
|
||
if let Some(sid) = resume_session_id {
|
||
if !sid.is_empty() {
|
||
payload["resumeSessionId"] = serde_json::Value::String(sid);
|
||
}
|
||
}
|
||
payload
|
||
},
|
||
"set-context" | "clear-context" => serde_json::json!({
|
||
"command": command,
|
||
"id": request_id,
|
||
"context": message
|
||
}),
|
||
_ => 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;
|
||
}
|
||
|
||
// Context aus DB laden (Schicht 1: Sticky Context)
|
||
let context = load_sticky_context_for_prompt(&app);
|
||
|
||
if context.is_some() {
|
||
println!("📌 Sticky Context geladen (~{} Token)", context.as_ref().map(|c| c.len() / 4).unwrap_or(0));
|
||
}
|
||
|
||
// Claude-Session-ID für Fortsetzung laden
|
||
let resume_session_id = load_claude_session_id(&app);
|
||
if resume_session_id.is_some() {
|
||
println!("🔗 Session fortsetzen mit Claude-ID: {:?}", resume_session_id);
|
||
}
|
||
|
||
send_to_bridge_full(&app, "message", &message, context, resume_session_id)?;
|
||
|
||
// Hinweis: Die eigentliche Antwort kommt über Events
|
||
Ok("Nachricht gesendet. Antwort folgt über Events.".to_string())
|
||
}
|
||
|
||
/// Claude-Session-ID der aktiven Session laden
|
||
fn load_claude_session_id(app: &AppHandle) -> Option<String> {
|
||
if let Some(db_state) = app.try_state::<Arc<Mutex<db::Database>>>() {
|
||
let db = db_state.lock().ok()?;
|
||
if let Ok(Some(session)) = db.get_active_session() {
|
||
return session.claude_session_id;
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
/// Sticky Context aus DB laden und als Prompt-Text rendern
|
||
fn load_sticky_context_for_prompt(app: &AppHandle) -> Option<String> {
|
||
use crate::context;
|
||
|
||
// Versuche Context zu laden
|
||
if let Some(db_state) = app.try_state::<Arc<Mutex<db::Database>>>() {
|
||
let db = db_state.lock().ok()?;
|
||
|
||
// Context-Tabellen erstellen falls nicht vorhanden
|
||
let _ = db.create_context_tables();
|
||
|
||
// Sticky Context laden
|
||
let entries = db.load_sticky_context().ok()?;
|
||
|
||
if entries.is_empty() {
|
||
return None;
|
||
}
|
||
|
||
let mut sticky = context::StickyContext::default();
|
||
|
||
for (key, value, _priority) in entries {
|
||
match key.as_str() {
|
||
"user_info" => sticky.user_info = Some(value),
|
||
k if k.starts_with("cred:") => {
|
||
if let Ok(cred) = serde_json::from_str::<context::CredentialHint>(&value) {
|
||
sticky.active_credentials.push(cred);
|
||
}
|
||
}
|
||
k if k.starts_with("project:") => {
|
||
if let Ok(proj) = serde_json::from_str::<context::ProjectInfo>(&value) {
|
||
sticky.current_project = Some(proj);
|
||
}
|
||
}
|
||
k if k.starts_with("rule:") => {
|
||
sticky.critical_rules.push(value);
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
let rendered = sticky.render();
|
||
if rendered.is_empty() {
|
||
None
|
||
} else {
|
||
Some(rendered)
|
||
}
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
|
||
/// 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())
|
||
}
|
||
|
||
/// Modell wechseln
|
||
#[tauri::command]
|
||
pub async fn set_model(app: AppHandle, model: String) -> Result<String, String> {
|
||
println!("🔄 Modell wechseln zu: {}", model);
|
||
|
||
// Modell in Settings speichern
|
||
if let Some(db_state) = app.try_state::<Arc<Mutex<crate::db::Database>>>() {
|
||
let db = db_state.lock().unwrap();
|
||
let _ = db.set_setting("claude_model", &model);
|
||
}
|
||
|
||
// 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)?;
|
||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||
}
|
||
|
||
// Modell an Bridge senden
|
||
send_to_bridge(&app, "set-model", &model)?;
|
||
|
||
Ok(model)
|
||
}
|
||
|
||
/// Verfügbare Modelle abrufen
|
||
#[tauri::command]
|
||
pub async fn get_available_models() -> Result<Vec<ModelInfo>, String> {
|
||
Ok(vec![
|
||
ModelInfo {
|
||
id: "haiku".to_string(),
|
||
name: "Claude Haiku".to_string(),
|
||
description: "Schnell & günstig".to_string(),
|
||
},
|
||
ModelInfo {
|
||
id: "sonnet".to_string(),
|
||
name: "Claude Sonnet".to_string(),
|
||
description: "Ausgewogen".to_string(),
|
||
},
|
||
ModelInfo {
|
||
id: "opus".to_string(),
|
||
name: "Claude Opus".to_string(),
|
||
description: "Leistungsstark".to_string(),
|
||
},
|
||
])
|
||
}
|
||
|
||
/// Aktuelles Modell aus Settings laden
|
||
#[tauri::command]
|
||
pub async fn get_current_model(app: AppHandle) -> Result<String, String> {
|
||
if let Some(db_state) = app.try_state::<Arc<Mutex<crate::db::Database>>>() {
|
||
let db = db_state.lock().unwrap();
|
||
if let Ok(Some(model)) = db.get_setting("claude_model") {
|
||
return Ok(model);
|
||
}
|
||
}
|
||
// Default
|
||
Ok("opus".to_string())
|
||
}
|
||
|
||
/// Agent-Modus setzen (solo, handlanger, experten, auto)
|
||
#[tauri::command]
|
||
pub async fn set_agent_mode(app: AppHandle, mode: String) -> Result<String, String> {
|
||
let valid_modes = ["solo", "handlanger", "experten", "auto"];
|
||
if !valid_modes.contains(&mode.as_str()) {
|
||
return Err(format!("Ungültiger Modus: {}. Verfügbar: {}", mode, valid_modes.join(", ")));
|
||
}
|
||
|
||
println!("🔄 Agent-Modus wechseln zu: {}", mode);
|
||
|
||
// Modus in Settings speichern
|
||
if let Some(db_state) = app.try_state::<Arc<Mutex<crate::db::Database>>>() {
|
||
let db = db_state.lock().unwrap();
|
||
let _ = db.set_setting("agent_mode", &mode);
|
||
}
|
||
|
||
// 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)?;
|
||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||
}
|
||
|
||
// Modus an Bridge senden
|
||
send_to_bridge(&app, "set-mode", &mode)?;
|
||
|
||
Ok(mode)
|
||
}
|
||
|
||
/// Aktuellen Agent-Modus aus Settings laden
|
||
#[tauri::command]
|
||
pub async fn get_agent_mode(app: AppHandle) -> Result<String, String> {
|
||
if let Some(db_state) = app.try_state::<Arc<Mutex<crate::db::Database>>>() {
|
||
let db = db_state.lock().unwrap();
|
||
if let Ok(Some(mode)) = db.get_setting("agent_mode") {
|
||
return Ok(mode);
|
||
}
|
||
}
|
||
// Default: solo
|
||
Ok("solo".to_string())
|
||
}
|
||
|
||
/// Modell-Info Struct
|
||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||
pub struct ModelInfo {
|
||
pub id: String,
|
||
pub name: String,
|
||
pub description: String,
|
||
}
|
||
|
||
/// Sticky Context Initialisierungs-Info
|
||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||
pub struct StickyContextInfo {
|
||
pub loaded: bool,
|
||
pub entries: usize,
|
||
pub estimated_tokens: usize,
|
||
pub has_user_info: bool,
|
||
pub has_project: bool,
|
||
pub credentials_count: usize,
|
||
pub rules_count: usize,
|
||
}
|
||
|
||
/// Sticky Context beim App-Start initialisieren
|
||
/// Lädt den Context aus der DB und sendet ihn an die Bridge
|
||
#[tauri::command]
|
||
pub async fn init_sticky_context(app: AppHandle) -> Result<StickyContextInfo, String> {
|
||
println!("📌 Initialisiere Sticky Context...");
|
||
|
||
// Context aus DB laden
|
||
let context = load_sticky_context_for_prompt(&app);
|
||
|
||
let mut info = StickyContextInfo {
|
||
loaded: false,
|
||
entries: 0,
|
||
estimated_tokens: 0,
|
||
has_user_info: false,
|
||
has_project: false,
|
||
credentials_count: 0,
|
||
rules_count: 0,
|
||
};
|
||
|
||
// Details aus DB laden für Info
|
||
if let Some(db_state) = app.try_state::<Arc<Mutex<crate::db::Database>>>() {
|
||
let db = db_state.lock().unwrap();
|
||
let _ = db.create_context_tables();
|
||
|
||
if let Ok(entries) = db.load_sticky_context() {
|
||
info.entries = entries.len();
|
||
|
||
for (key, _value, _priority) in &entries {
|
||
match key.as_str() {
|
||
"user_info" => info.has_user_info = true,
|
||
k if k.starts_with("cred:") => info.credentials_count += 1,
|
||
k if k.starts_with("project:") => info.has_project = true,
|
||
k if k.starts_with("rule:") => info.rules_count += 1,
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if let Some(ref ctx) = context {
|
||
info.loaded = true;
|
||
info.estimated_tokens = ctx.len() / 4;
|
||
|
||
// 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;
|
||
}
|
||
|
||
// Context an Bridge senden
|
||
let _ = send_to_bridge(&app, "set-context", ctx);
|
||
|
||
println!("✅ Sticky Context geladen: {} Einträge, ~{} Token", info.entries, info.estimated_tokens);
|
||
} else {
|
||
println!("ℹ️ Kein Sticky Context konfiguriert");
|
||
}
|
||
|
||
Ok(info)
|
||
}
|