claude-desktop/src-tauri/src/hooks.rs
Eddy 0a447591da
All checks were successful
Build AppImage / build (push) Successful in 7m51s
Phase 1.5: Aktivierung & Quick-Wins [appimage]
- KB-Hints werden automatisch in jeden Claude-Prompt injiziert
- SQL-Queries berücksichtigen jetzt Priority (DESC)
- Voice-zu-Claude-Pipeline: Sprache → Transkription → Claude → TTS
- Hook-System feuert echte Events (SessionStart, Pre/PostToolUse)
- Pattern-Detektion bei Tool-Fehlern aktiviert
- Slash-Command Autocomplete mit CommandPalette
- Updater abgesichert: Lock-Datei, Prozess-Guard, Bestätigungs-Dialog
- ROADMAP.md und CHANGELOG.md aktualisiert

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 13:00:40 +02:00

271 lines
8.3 KiB
Rust

// Claude Desktop — Hook-System
// Zentraler Dispatcher + Audit-Log fuer automatische Aktionen
// (SessionStart, PreToolUse, PostToolUse, BeforeCompacting, AfterCompacting)
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use std::collections::HashMap;
use tauri::{AppHandle, Emitter};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HookEvent {
SessionStart,
PreToolUse,
PostToolUse,
BeforeCompacting,
AfterCompacting,
ContextFailure,
AgentStarted,
}
impl HookEvent {
pub fn as_str(&self) -> &'static str {
match self {
HookEvent::SessionStart => "SessionStart",
HookEvent::PreToolUse => "PreToolUse",
HookEvent::PostToolUse => "PostToolUse",
HookEvent::BeforeCompacting => "BeforeCompacting",
HookEvent::AfterCompacting => "AfterCompacting",
HookEvent::ContextFailure => "ContextFailure",
HookEvent::AgentStarted => "AgentStarted",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"SessionStart" => Some(HookEvent::SessionStart),
"PreToolUse" => Some(HookEvent::PreToolUse),
"PostToolUse" => Some(HookEvent::PostToolUse),
"BeforeCompacting" => Some(HookEvent::BeforeCompacting),
"AfterCompacting" => Some(HookEvent::AfterCompacting),
"ContextFailure" => Some(HookEvent::ContextFailure),
"AgentStarted" => Some(HookEvent::AgentStarted),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HookConfig {
pub name: String,
pub event: String,
pub enabled: bool,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HookExecution {
pub timestamp: String,
pub event: String,
pub hook_name: String,
pub duration_ms: u64,
pub success: bool,
pub summary: String,
}
/// Hook-Manager — haelt Registry und Ausfuehrungs-Log
pub struct HookManager {
pub hooks: HashMap<String, Vec<HookConfig>>,
pub executions: Vec<HookExecution>,
pub max_log_size: usize,
}
impl Default for HookManager {
fn default() -> Self {
let mut mgr = HookManager {
hooks: HashMap::new(),
executions: Vec::new(),
max_log_size: 500,
};
mgr.register_builtin_hooks();
mgr
}
}
impl HookManager {
/// Eingebaute Hooks registrieren
fn register_builtin_hooks(&mut self) {
let builtins = vec![
HookConfig {
name: "load-sticky-context".into(),
event: "SessionStart".into(),
enabled: true,
description: "Laedt Sticky-Context bei Session-Start".into(),
},
HookConfig {
name: "inject-knowledge-hints".into(),
event: "PreToolUse".into(),
enabled: true,
description: "Injiziert relevante KB-Eintraege vor Tool-Ausfuehrung".into(),
},
HookConfig {
name: "save-failure-pattern".into(),
event: "PostToolUse".into(),
enabled: true,
description: "Speichert Fehler-Pattern nach fehlgeschlagenen Tools".into(),
},
HookConfig {
name: "extract-critical-context".into(),
event: "BeforeCompacting".into(),
enabled: true,
description: "Extrahiert kritischen Kontext vor Compacting".into(),
},
HookConfig {
name: "reinject-context".into(),
event: "AfterCompacting".into(),
enabled: true,
description: "Injiziert Sticky+Project-Context nach Compacting".into(),
},
];
for hook in builtins {
self.hooks
.entry(hook.event.clone())
.or_default()
.push(hook);
}
}
pub fn fire(&mut self, event: &HookEvent, summary: String) -> Vec<String> {
let event_name = event.as_str().to_string();
let mut fired_names = Vec::new();
if let Some(hooks) = self.hooks.get(&event_name) {
for hook in hooks.iter().filter(|h| h.enabled) {
fired_names.push(hook.name.clone());
let execution = HookExecution {
timestamp: chrono::Utc::now().to_rfc3339(),
event: event_name.clone(),
hook_name: hook.name.clone(),
duration_ms: 0,
success: true,
summary: summary.clone(),
};
self.executions.push(execution);
if self.executions.len() > self.max_log_size {
self.executions.remove(0);
}
}
}
fired_names
}
pub fn set_enabled(&mut self, event: &str, hook_name: &str, enabled: bool) -> bool {
if let Some(hooks) = self.hooks.get_mut(event) {
for hook in hooks.iter_mut() {
if hook.name == hook_name {
hook.enabled = enabled;
return true;
}
}
}
false
}
pub fn list_all(&self) -> Vec<HookConfig> {
self.hooks.values().flatten().cloned().collect()
}
pub fn recent_executions(&self, limit: usize) -> Vec<HookExecution> {
let start = self.executions.len().saturating_sub(limit);
self.executions[start..].to_vec()
}
}
pub type HookState = Arc<Mutex<HookManager>>;
// ============ Tauri Commands ============
#[tauri::command]
pub async fn list_hooks(state: tauri::State<'_, HookState>) -> Result<Vec<HookConfig>, String> {
let mgr = state.lock().map_err(|e| e.to_string())?;
Ok(mgr.list_all())
}
#[tauri::command]
pub async fn set_hook_enabled(
state: tauri::State<'_, HookState>,
event: String,
hook_name: String,
enabled: bool,
) -> Result<bool, String> {
let mut mgr = state.lock().map_err(|e| e.to_string())?;
Ok(mgr.set_enabled(&event, &hook_name, enabled))
}
#[tauri::command]
pub async fn get_hook_executions(
state: tauri::State<'_, HookState>,
limit: Option<usize>,
) -> Result<Vec<HookExecution>, String> {
let mgr = state.lock().map_err(|e| e.to_string())?;
Ok(mgr.recent_executions(limit.unwrap_or(100)))
}
#[tauri::command]
pub async fn fire_hook(
app: AppHandle,
state: tauri::State<'_, HookState>,
event: String,
summary: String,
) -> Result<Vec<String>, String> {
let hook_event = HookEvent::from_str(&event)
.ok_or_else(|| format!("Unbekanntes Hook-Event: {}", event))?;
let fired = {
let mut mgr = state.lock().map_err(|e| e.to_string())?;
mgr.fire(&hook_event, summary.clone())
};
// Event ans Frontend senden (fuer Live-Log im UI)
let _ = app.emit(
"hook-fired",
serde_json::json!({
"event": event,
"hooks": fired,
"summary": summary,
}),
);
// Spezifische Hook-Events dispatchen — Frontend kann gezielt darauf reagieren
// Summary wird als JSON-Payload durchgereicht (Tool-Name, Argumente, Ergebnis etc.)
match hook_event {
HookEvent::SessionStart => {
let _ = app.emit("hook-session-start", serde_json::json!({
"hooks": fired,
"payload": summary,
}));
}
HookEvent::PreToolUse => {
let _ = app.emit("hook-pre-tool-use", serde_json::json!({
"hooks": fired,
"payload": summary,
}));
}
HookEvent::PostToolUse => {
let _ = app.emit("hook-post-tool-use", serde_json::json!({
"hooks": fired,
"payload": summary,
}));
}
HookEvent::BeforeCompacting => {
let _ = app.emit("hook-before-compacting", serde_json::json!({
"hooks": fired,
"payload": summary,
}));
}
HookEvent::AfterCompacting => {
let _ = app.emit("hook-after-compacting", serde_json::json!({
"hooks": fired,
"payload": summary,
}));
}
// ContextFailure + AgentStarted: aktuell kein eigenes Frontend-Event noetig
_ => {}
}
Ok(fired)
}