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>
234 lines
7 KiB
Rust
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)
|
|
}
|