claude-desktop/src-tauri/src/commands.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

140 lines
4.8 KiB
Rust

// Slash-Command Registry — scannt ~/.claude/commands/ und ~/.claude/skills/
// und liefert eine kombinierte Liste inkl. Built-in-Commands an das Frontend.
use serde::Serialize;
use std::fs;
use std::path::PathBuf;
/// Ein Slash-Command mit Metadaten
#[derive(Debug, Clone, Serialize)]
pub struct SlashCommand {
/// Name des Commands (ohne führenden Slash)
pub name: String,
/// Beschreibung (erste Zeile der .md-Datei oder fest hinterlegt)
pub description: String,
/// Kategorie: "builtin", "custom", "skill"
pub category: String,
/// Herkunft: Dateipfad oder "builtin"
pub source: String,
}
/// Liest die erste nicht-leere Zeile einer Datei als Beschreibung.
/// Entfernt dabei führende Markdown-Header-Zeichen (#).
fn read_first_line(path: &PathBuf) -> String {
match fs::read_to_string(path) {
Ok(content) => {
content
.lines()
.find(|line| !line.trim().is_empty())
.map(|line| line.trim().trim_start_matches('#').trim().to_string())
.unwrap_or_else(|| "Keine Beschreibung".into())
}
Err(_) => "Datei nicht lesbar".into(),
}
}
/// Scannt alle verfügbaren Slash-Commands aus verschiedenen Quellen
fn scan_commands() -> Vec<SlashCommand> {
let mut commands: Vec<SlashCommand> = Vec::new();
// 1. Built-in-Commands (hardcoded)
let builtins = vec![
("help", "Hilfe und verfügbare Commands anzeigen"),
("clear", "Chat-Verlauf leeren"),
("compact", "Konversation kompaktieren (Token sparen)"),
("model", "KI-Modell wechseln (Sonnet/Opus/Haiku)"),
("cost", "Aktuelle Token-Kosten anzeigen"),
("doctor", "System-Diagnose und Health-Check"),
("review", "Code-Review für aktuelles Projekt"),
];
for (name, desc) in builtins {
commands.push(SlashCommand {
name: name.to_string(),
description: desc.to_string(),
category: "builtin".to_string(),
source: "builtin".to_string(),
});
}
// Home-Verzeichnis ermitteln (ohne externe Dependency)
let home = match std::env::var("HOME") {
Ok(h) => PathBuf::from(h),
Err(_) => {
eprintln!("⚠️ HOME-Umgebungsvariable nicht gesetzt");
return commands;
}
};
// 2. Custom Commands aus ~/.claude/commands/*.md
let commands_dir = home.join(".claude").join("commands");
if commands_dir.is_dir() {
if let Ok(entries) = fs::read_dir(&commands_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().map_or(false, |ext| ext == "md") {
let name = path
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let description = read_first_line(&path);
commands.push(SlashCommand {
name,
description,
category: "custom".to_string(),
source: path.to_string_lossy().to_string(),
});
}
}
}
}
// 3. Skills aus ~/.claude/skills/*/SKILL.md
let skills_dir = home.join(".claude").join("skills");
if skills_dir.is_dir() {
if let Ok(entries) = fs::read_dir(&skills_dir) {
for entry in entries.flatten() {
let skill_dir = entry.path();
if skill_dir.is_dir() {
let skill_md = skill_dir.join("SKILL.md");
if skill_md.exists() {
let name = skill_dir
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let description = read_first_line(&skill_md);
commands.push(SlashCommand {
name,
description,
category: "skill".to_string(),
source: skill_md.to_string_lossy().to_string(),
});
}
}
}
}
}
// Alphabetisch sortieren (Built-ins zuerst, dann Custom, dann Skills)
commands.sort_by(|a, b| {
let cat_order = |c: &str| match c {
"builtin" => 0,
"custom" => 1,
"skill" => 2,
_ => 3,
};
cat_order(&a.category)
.cmp(&cat_order(&b.category))
.then_with(|| a.name.cmp(&b.name))
});
commands
}
/// Tauri-Command: Gibt alle verfügbaren Slash-Commands zurück
#[tauri::command]
pub fn get_slash_commands() -> Vec<SlashCommand> {
scan_commands()
}