claude-desktop/src-tauri/src/hooks.rs
Eddy 120715982b Phasen 12-15: Hooks, VSCodium-Bridge, Programm-Steuerung, Schulungsmodus
Phase 12 Hook-System (hooks.rs + HooksPanel):
- HookManager mit Event-Registry + Ausfuehrungs-Log
- 5 Built-in Hooks (SessionStart, PreToolUse, PostToolUse,
  BeforeCompacting, AfterCompacting)
- Tauri-Commands: list_hooks, set_hook_enabled, get_hook_executions, fire_hook
- HooksPanel.svelte mit Live-Ausfuehrungs-Log

Phase 13 VSCodium-Integration:
- vscode-extension/: WebSocket-Server auf Port 7890
  (Commands: openFile, goToLine, formatDocument, findInFiles,
   openTerminal, getStatus, executeCommand, ping)
- src-tauri/src/ide.rs: WebSocket-Client via tokio-tungstenite
- IdePanel.svelte: Status, Port-Konfig, Ping-Test, Live-Anzeige aktive Datei

Phase 14 Programm-Steuerung (programs.rs + ProgramsPanel):
- D-Bus: dbus_call + dbus_list_services
- Xvfb: start/stop/status + screenshot (scrot)
- Playwright-Info (MCP-Verweis)
- ProgramsPanel mit 4 Sektionen (VSCodium, Playwright, D-Bus, Xvfb)

Phase 15 Schulungsmodus (teaching.rs + presentation/+page.svelte):
- Separates Tauri-Webview-Fenster
- MermaidDiagram.svelte (dynamic import mermaid)
- AnimatedCode.svelte mit WPM-Steuerung
- Tauri-Commands: presentation_open/close/send_slide/clear
- 🎓-Button in der Titelbar
- Capabilities um core:webview:allow-create-webview-window erweitert

Deps:
- Cargo: +tokio-tungstenite 0.23, +futures-util 0.3
- npm: +mermaid ^11.4.0 (npm install erforderlich)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:10:41 +02:00

234 lines
7 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,
}),
);
Ok(fired)
}