feat: Guard-Rails UI, D-Bus Aktionen, Screenshot-Analyse [appimage]
All checks were successful
Build AppImage / build (push) Successful in 8m16s
All checks were successful
Build AppImage / build (push) Successful in 8m16s
Guard-Rails: Live-Feed mit Risiko-Badges, 3 Tabs, Ein-Klick-Freigabe D-Bus: 10 Desktop-Aktionen (Dolphin, Kate, Konsole, Firefox, Notify) Screenshot: Region/Vollbild via spectacle/scrot, Vorschau + Chat-Send Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
87ba8f7bdf
commit
d4c57b777a
7 changed files with 954 additions and 131 deletions
|
|
@ -9,6 +9,9 @@ Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/).
|
||||||
## [Unreleased] - 2026-04-21
|
## [Unreleased] - 2026-04-21
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
|
- **Guard-Rails UI (Live)**: 3-Tab-Ansicht (Live-Feed/Regeln/Blockiert), Risiko-Statistik-Leiste, Ein-Klick-Freigabe bei Bestätigungsbedarf, guard-check Events vom Backend (`GuardRailsPanel.svelte`, `guard.rs`)
|
||||||
|
- **D-Bus Desktop-Aktionen**: 10 vordefinierte Aktionen (Dolphin, Kate, Konsole, Firefox, Notify, Lock Screen), Aktionen-Grid im ProgramsPanel, CLI/GUI-Unterscheidung (`programs.rs`, `ProgramsPanel.svelte`)
|
||||||
|
- **Screenshot-Analyse**: Bildschirmbereich oder Vollbild capturen via spectacle/scrot/gnome-screenshot, Vorschau im Panel, "An Claude senden" Button (`programs.rs`, `ProgramsPanel.svelte`)
|
||||||
- **Projekt-Wechsel**: Ein-Klick-Projektwechsel in der Sidebar — Dropdown mit Projektliste, Hinzufügen/Entfernen, Working-Dir + Sticky-Context wird automatisch umgeschaltet (`SessionList.svelte`, `db.rs`)
|
- **Projekt-Wechsel**: Ein-Klick-Projektwechsel in der Sidebar — Dropdown mit Projektliste, Hinzufügen/Entfernen, Working-Dir + Sticky-Context wird automatisch umgeschaltet (`SessionList.svelte`, `db.rs`)
|
||||||
- **File-Drop auf Chat**: Dateien per Drag & Drop auf den Chat ziehen — Text-Dateien als Code-Block, Bilder als Base64, Spracherkennung, 500KB-Limit (`ChatPanel.svelte`)
|
- **File-Drop auf Chat**: Dateien per Drag & Drop auf den Chat ziehen — Text-Dateien als Code-Block, Bilder als Base64, Spracherkennung, 500KB-Limit (`ChatPanel.svelte`)
|
||||||
- **Persistent Memory**: Auto-Load Memory-Einträge werden bei jeder Nachricht in den Claude-Context injiziert — Cross-Session Gedächtnis für Patterns, Zugänge, Präferenzen (`memory.rs`, `claude.rs`)
|
- **Persistent Memory**: Auto-Load Memory-Einträge werden bei jeder Nachricht in den Claude-Context injiziert — Cross-Session Gedächtnis für Patterns, Zugänge, Präferenzen (`memory.rs`, `claude.rs`)
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ Alles aus Phase 1-16 ist implementiert und funktionsfaehig:
|
||||||
|---------|-----------|--------|
|
|---------|-----------|--------|
|
||||||
| ✅ Projekt-Wechsel | `db.rs`, `SessionList.svelte` | Ein Klick wechselt Projekt (CWD, Context, KB-Filter) |
|
| ✅ Projekt-Wechsel | `db.rs`, `SessionList.svelte` | Ein Klick wechselt Projekt (CWD, Context, KB-Filter) |
|
||||||
| MCP-Hub nativ | `claude-bridge.js` | ⬜ Alle MCP-Server direkt nutzbar (Docker, Forgejo, DB) ohne CLI-Umweg |
|
| MCP-Hub nativ | `claude-bridge.js` | ⬜ Alle MCP-Server direkt nutzbar (Docker, Forgejo, DB) ohne CLI-Umweg |
|
||||||
| Guard-Rails UI | `guard.rs`, `GuardPanel.svelte` | ⬜ Live-Anzeige was Claude darf/nicht darf, Ein-Klick-Freigabe |
|
| ✅ Guard-Rails UI | `guard.rs`, `GuardRailsPanel.svelte` | Live-Feed, Risiko-Statistik, Ein-Klick-Freigabe, 3 Tabs |
|
||||||
| ✅ Persistent Memory | `memory.rs`, `claude.rs` | Auto-Load Eintraege in Context, Cross-Session Gedaechtnis |
|
| ✅ Persistent Memory | `memory.rs`, `claude.rs` | Auto-Load Eintraege in Context, Cross-Session Gedaechtnis |
|
||||||
| ✅ Quick-Actions | `QuickActions.svelte`, `ChatPanel.svelte` | Ctrl+K Palette: Deploy, Build, Test, Commit, Git, Navigation |
|
| ✅ Quick-Actions | `QuickActions.svelte`, `ChatPanel.svelte` | Ctrl+K Palette: Deploy, Build, Test, Commit, Git, Navigation |
|
||||||
| ✅ Voice-Conversation | `voice.rs`, `VoicePanel.svelte` | Lokales Whisper STT + Piper TTS, VAD, Gespraechsmodus |
|
| ✅ Voice-Conversation | `voice.rs`, `VoicePanel.svelte` | Lokales Whisper STT + Piper TTS, VAD, Gespraechsmodus |
|
||||||
|
|
@ -91,10 +91,10 @@ Alles aus Phase 1-16 ist implementiert und funktionsfaehig:
|
||||||
|
|
||||||
| Feature | Datei(en) | Beschreibung |
|
| Feature | Datei(en) | Beschreibung |
|
||||||
|---------|-----------|--------------|
|
|---------|-----------|--------------|
|
||||||
| D-Bus Actions | `programs.rs` | ⬜ Vordefinierte Aktionen: Dolphin oeffnen, Kate starten, Notifications |
|
| ✅ D-Bus Actions | `programs.rs`, `ProgramsPanel.svelte` | 10 Aktionen: Dolphin, Kate, Konsole, Firefox, Notify, Lock |
|
||||||
| ✅ Clipboard-Watch | `clipboard.rs` | Claude reagiert auf Clipboard-Inhalt (Code/URL/Fehler erkennen) |
|
| ✅ Clipboard-Watch | `clipboard.rs` | Claude reagiert auf Clipboard-Inhalt (Code/URL/Fehler erkennen) |
|
||||||
| ✅ File-Drop | `ChatPanel.svelte` | Dateien auf Chat droppen → Claude analysiert/bearbeitet |
|
| ✅ File-Drop | `ChatPanel.svelte` | Dateien auf Chat droppen → Claude analysiert/bearbeitet |
|
||||||
| Screenshot-Analyse | `programs.rs` | ⬜ Bildschirmbereich markieren → Claude beschreibt/debuggt UI |
|
| ✅ Screenshot-Analyse | `programs.rs`, `ProgramsPanel.svelte` | Spectacle/Scrot Region-Capture, Vorschau, an Chat senden |
|
||||||
| ✅ Global Hotkey | `lib.rs` | Super+C oeffnet Claude-Eingabe von ueberall |
|
| ✅ Global Hotkey | `lib.rs` | Super+C oeffnet Claude-Eingabe von ueberall |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// Risiko-Klassifikation und Freigabe-Management
|
// Risiko-Klassifikation und Freigabe-Management
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Emitter, Manager};
|
||||||
|
|
||||||
/// Risiko-Level einer Aktion
|
/// Risiko-Level einer Aktion
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
|
@ -354,8 +354,14 @@ pub async fn check_action(
|
||||||
|
|
||||||
let risk = guard.classify_risk(&tool, &command, path.as_deref());
|
let risk = guard.classify_risk(&tool, &command, path.as_deref());
|
||||||
|
|
||||||
|
let ts = chrono::Utc::now().timestamp_millis();
|
||||||
|
|
||||||
// Wenn blockiert, sofort ablehnen
|
// Wenn blockiert, sofort ablehnen
|
||||||
if risk == RiskLevel::Blocked {
|
if risk == RiskLevel::Blocked {
|
||||||
|
let _ = app.emit("guard-check", serde_json::json!({
|
||||||
|
"tool": tool, "command": command, "risk": "blocked",
|
||||||
|
"allowed": false, "needs_confirmation": false, "timestamp": ts
|
||||||
|
}));
|
||||||
return Ok(serde_json::json!({
|
return Ok(serde_json::json!({
|
||||||
"allowed": false,
|
"allowed": false,
|
||||||
"risk": "blocked",
|
"risk": "blocked",
|
||||||
|
|
@ -365,9 +371,15 @@ pub async fn check_action(
|
||||||
|
|
||||||
// Permission prüfen
|
// Permission prüfen
|
||||||
if let Some(perm) = guard.check_permission(&tool, &command, path.as_deref()) {
|
if let Some(perm) = guard.check_permission(&tool, &command, path.as_deref()) {
|
||||||
|
let perm_allowed = perm.action == PermissionAction::Allow;
|
||||||
|
let risk_str = format!("{:?}", risk).to_lowercase();
|
||||||
|
let _ = app.emit("guard-check", serde_json::json!({
|
||||||
|
"tool": tool, "command": command, "risk": risk_str,
|
||||||
|
"allowed": perm_allowed, "needs_confirmation": false, "timestamp": ts
|
||||||
|
}));
|
||||||
return Ok(serde_json::json!({
|
return Ok(serde_json::json!({
|
||||||
"allowed": perm.action == PermissionAction::Allow,
|
"allowed": perm_allowed,
|
||||||
"risk": format!("{:?}", risk).to_lowercase(),
|
"risk": risk_str,
|
||||||
"matched_rule": perm.id
|
"matched_rule": perm.id
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
@ -375,10 +387,24 @@ pub async fn check_action(
|
||||||
// Kein Match - Frontend muss fragen
|
// Kein Match - Frontend muss fragen
|
||||||
let suggested = guard.suggest_pattern(&tool, &command);
|
let suggested = guard.suggest_pattern(&tool, &command);
|
||||||
|
|
||||||
|
let risk_str = format!("{:?}", risk).to_lowercase();
|
||||||
|
let allowed = risk == RiskLevel::Safe;
|
||||||
|
let needs_confirm = risk != RiskLevel::Safe;
|
||||||
|
|
||||||
|
// Live-Event ans Frontend senden
|
||||||
|
let _ = app.emit("guard-check", serde_json::json!({
|
||||||
|
"tool": tool,
|
||||||
|
"command": command,
|
||||||
|
"risk": risk_str,
|
||||||
|
"allowed": allowed,
|
||||||
|
"needs_confirmation": needs_confirm,
|
||||||
|
"timestamp": chrono::Utc::now().timestamp_millis()
|
||||||
|
}));
|
||||||
|
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"allowed": risk == RiskLevel::Safe,
|
"allowed": allowed,
|
||||||
"risk": format!("{:?}", risk).to_lowercase(),
|
"risk": risk_str,
|
||||||
"needs_confirmation": risk != RiskLevel::Safe,
|
"needs_confirmation": needs_confirm,
|
||||||
"suggested_pattern": suggested
|
"suggested_pattern": suggested
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,9 @@ pub fn run() {
|
||||||
// Programm-Steuerung (D-Bus, Xvfb, Playwright-Info)
|
// Programm-Steuerung (D-Bus, Xvfb, Playwright-Info)
|
||||||
programs::dbus_call,
|
programs::dbus_call,
|
||||||
programs::dbus_list_services,
|
programs::dbus_list_services,
|
||||||
|
programs::dbus_list_actions,
|
||||||
|
programs::dbus_run_action,
|
||||||
|
programs::capture_screenshot,
|
||||||
programs::xvfb_start,
|
programs::xvfb_start,
|
||||||
programs::xvfb_stop,
|
programs::xvfb_stop,
|
||||||
programs::xvfb_status,
|
programs::xvfb_status,
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,157 @@ pub async fn dbus_list_services(session: Option<bool>) -> Result<Vec<String>, St
|
||||||
Ok(services)
|
Ok(services)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ D-Bus Vordefinierte Aktionen ============
|
||||||
|
|
||||||
|
/// Vordefinierte Desktop-Aktion
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DesktopAction {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub icon: String,
|
||||||
|
pub category: String,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Liste aller verfuegbaren Desktop-Aktionen
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn dbus_list_actions() -> Result<Vec<DesktopAction>, String> {
|
||||||
|
Ok(vec![
|
||||||
|
DesktopAction {
|
||||||
|
id: "dolphin".into(), name: "Dolphin öffnen".into(),
|
||||||
|
icon: "📁".into(), category: "Dateimanager".into(),
|
||||||
|
description: "Dolphin Dateimanager im aktuellen Verzeichnis öffnen".into(),
|
||||||
|
},
|
||||||
|
DesktopAction {
|
||||||
|
id: "dolphin_path".into(), name: "Dolphin (Pfad)".into(),
|
||||||
|
icon: "📂".into(), category: "Dateimanager".into(),
|
||||||
|
description: "Dolphin in einem bestimmten Verzeichnis öffnen".into(),
|
||||||
|
},
|
||||||
|
DesktopAction {
|
||||||
|
id: "kate".into(), name: "Kate öffnen".into(),
|
||||||
|
icon: "📝".into(), category: "Editor".into(),
|
||||||
|
description: "Kate Texteditor starten".into(),
|
||||||
|
},
|
||||||
|
DesktopAction {
|
||||||
|
id: "kate_file".into(), name: "Kate (Datei)".into(),
|
||||||
|
icon: "📄".into(), category: "Editor".into(),
|
||||||
|
description: "Datei in Kate öffnen".into(),
|
||||||
|
},
|
||||||
|
DesktopAction {
|
||||||
|
id: "notify".into(), name: "Benachrichtigung".into(),
|
||||||
|
icon: "🔔".into(), category: "System".into(),
|
||||||
|
description: "KDE-Desktop-Notification senden".into(),
|
||||||
|
},
|
||||||
|
DesktopAction {
|
||||||
|
id: "konsole".into(), name: "Konsole öffnen".into(),
|
||||||
|
icon: "🖥️".into(), category: "Terminal".into(),
|
||||||
|
description: "Konsole Terminal-Emulator starten".into(),
|
||||||
|
},
|
||||||
|
DesktopAction {
|
||||||
|
id: "konsole_cmd".into(), name: "Konsole (Befehl)".into(),
|
||||||
|
icon: "⚡".into(), category: "Terminal".into(),
|
||||||
|
description: "Befehl in Konsole ausführen".into(),
|
||||||
|
},
|
||||||
|
DesktopAction {
|
||||||
|
id: "firefox".into(), name: "Firefox öffnen".into(),
|
||||||
|
icon: "🌐".into(), category: "Browser".into(),
|
||||||
|
description: "Firefox-Browser starten".into(),
|
||||||
|
},
|
||||||
|
DesktopAction {
|
||||||
|
id: "firefox_url".into(), name: "Firefox (URL)".into(),
|
||||||
|
icon: "🔗".into(), category: "Browser".into(),
|
||||||
|
description: "URL in Firefox öffnen".into(),
|
||||||
|
},
|
||||||
|
DesktopAction {
|
||||||
|
id: "lock_screen".into(), name: "Bildschirm sperren".into(),
|
||||||
|
icon: "🔒".into(), category: "System".into(),
|
||||||
|
description: "KDE Bildschirmsperre aktivieren".into(),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fuehrt eine vordefinierte Desktop-Aktion aus
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn dbus_run_action(
|
||||||
|
action_id: String,
|
||||||
|
arg: Option<String>,
|
||||||
|
) -> Result<DbusCallResult, String> {
|
||||||
|
match action_id.as_str() {
|
||||||
|
"dolphin" => {
|
||||||
|
let dir = arg.unwrap_or_else(|| ".".into());
|
||||||
|
run_desktop_cmd("dolphin", &[&dir])
|
||||||
|
}
|
||||||
|
"dolphin_path" => {
|
||||||
|
let path = arg.ok_or("Pfad-Argument fehlt")?;
|
||||||
|
run_desktop_cmd("dolphin", &[&path])
|
||||||
|
}
|
||||||
|
"kate" => {
|
||||||
|
run_desktop_cmd("kate", &[])
|
||||||
|
}
|
||||||
|
"kate_file" => {
|
||||||
|
let file = arg.ok_or("Datei-Argument fehlt")?;
|
||||||
|
run_desktop_cmd("kate", &[&file])
|
||||||
|
}
|
||||||
|
"notify" => {
|
||||||
|
let msg = arg.unwrap_or_else(|| "Claude Desktop Benachrichtigung".into());
|
||||||
|
// KDE notify-send
|
||||||
|
run_desktop_cmd("notify-send", &["Claude Desktop", &msg])
|
||||||
|
}
|
||||||
|
"konsole" => {
|
||||||
|
run_desktop_cmd("konsole", &[])
|
||||||
|
}
|
||||||
|
"konsole_cmd" => {
|
||||||
|
let cmd = arg.ok_or("Befehl-Argument fehlt")?;
|
||||||
|
run_desktop_cmd("konsole", &["-e", &cmd])
|
||||||
|
}
|
||||||
|
"firefox" => {
|
||||||
|
run_desktop_cmd("firefox", &[])
|
||||||
|
}
|
||||||
|
"firefox_url" => {
|
||||||
|
let url = arg.ok_or("URL-Argument fehlt")?;
|
||||||
|
// Grundlegende URL-Validierung
|
||||||
|
if !url.starts_with("http://") && !url.starts_with("https://") {
|
||||||
|
return Err("URL muss mit http:// oder https:// beginnen".into());
|
||||||
|
}
|
||||||
|
run_desktop_cmd("firefox", &[&url])
|
||||||
|
}
|
||||||
|
"lock_screen" => {
|
||||||
|
// loginctl lock-session
|
||||||
|
run_desktop_cmd("loginctl", &["lock-session"])
|
||||||
|
}
|
||||||
|
_ => Err(format!("Unbekannte Aktion: {}", action_id)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hilfsfunktion: Desktop-Programm starten
|
||||||
|
fn run_desktop_cmd(program: &str, args: &[&str]) -> Result<DbusCallResult, String> {
|
||||||
|
// Programm im Hintergrund starten (nicht blockierend)
|
||||||
|
let mut cmd = Command::new(program);
|
||||||
|
cmd.args(args);
|
||||||
|
|
||||||
|
// Bei GUI-Apps: spawn (nicht blockieren), bei CLI-Tools: output (Ergebnis lesen)
|
||||||
|
let is_cli = matches!(program, "notify-send" | "loginctl" | "dbus-send");
|
||||||
|
|
||||||
|
if is_cli {
|
||||||
|
let output = cmd.output().map_err(|e| format!("{} Fehler: {}", program, e))?;
|
||||||
|
Ok(DbusCallResult {
|
||||||
|
success: output.status.success(),
|
||||||
|
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||||
|
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||||
|
exit_code: output.status.code().unwrap_or(-1),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// GUI-App im Hintergrund starten
|
||||||
|
cmd.spawn().map_err(|e| format!("{} konnte nicht gestartet werden: {}", program, e))?;
|
||||||
|
Ok(DbusCallResult {
|
||||||
|
success: true,
|
||||||
|
stdout: format!("{} gestartet", program),
|
||||||
|
stderr: String::new(),
|
||||||
|
exit_code: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============ Xvfb (Virtuelles Display) ============
|
// ============ Xvfb (Virtuelles Display) ============
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -219,6 +370,86 @@ pub async fn xvfb_screenshot(display_num: Option<u16>) -> Result<String, String>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ Screenshot-Analyse ============
|
||||||
|
|
||||||
|
/// Macht einen Screenshot (Bereich oder Vollbild) und gibt Base64-PNG zurueck
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn capture_screenshot(
|
||||||
|
region: Option<bool>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let tmp = std::env::temp_dir().join(format!("claude-screenshot-{}.png", uuid::Uuid::new_v4()));
|
||||||
|
let tmp_path = tmp.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
let want_region = region.unwrap_or(true);
|
||||||
|
|
||||||
|
// Versuch 1: spectacle (KDE)
|
||||||
|
if Command::new("which").arg("spectacle").output().map(|o| o.status.success()).unwrap_or(false) {
|
||||||
|
let mut args = vec![
|
||||||
|
"-b".to_string(), // Hintergrund (kein GUI)
|
||||||
|
"-n".to_string(), // Nicht interaktiv speichern
|
||||||
|
"-o".to_string(), tmp_path.clone(),
|
||||||
|
];
|
||||||
|
if want_region {
|
||||||
|
args.push("-r".to_string()); // Region auswählen
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = Command::new("spectacle")
|
||||||
|
.args(&args)
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("spectacle Fehler: {}", e))?;
|
||||||
|
|
||||||
|
if result.status.success() && tmp.exists() {
|
||||||
|
let bytes = std::fs::read(&tmp).map_err(|e| e.to_string())?;
|
||||||
|
let _ = std::fs::remove_file(&tmp);
|
||||||
|
if bytes.is_empty() {
|
||||||
|
return Err("Screenshot abgebrochen (keine Auswahl)".into());
|
||||||
|
}
|
||||||
|
use base64::Engine;
|
||||||
|
return Ok(base64::engine::general_purpose::STANDARD.encode(&bytes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Versuch 2: scrot (einfacher, aber kein interaktiver Bereich ohne extra Tools)
|
||||||
|
if Command::new("which").arg("scrot").output().map(|o| o.status.success()).unwrap_or(false) {
|
||||||
|
let mut args = vec![tmp_path.clone()];
|
||||||
|
if want_region {
|
||||||
|
args.insert(0, "-s".to_string()); // Select-Modus
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = Command::new("scrot")
|
||||||
|
.args(&args)
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("scrot Fehler: {}", e))?;
|
||||||
|
|
||||||
|
if result.status.success() && tmp.exists() {
|
||||||
|
let bytes = std::fs::read(&tmp).map_err(|e| e.to_string())?;
|
||||||
|
let _ = std::fs::remove_file(&tmp);
|
||||||
|
use base64::Engine;
|
||||||
|
return Ok(base64::engine::general_purpose::STANDARD.encode(&bytes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Versuch 3: gnome-screenshot
|
||||||
|
if Command::new("which").arg("gnome-screenshot").output().map(|o| o.status.success()).unwrap_or(false) {
|
||||||
|
let mut cmd = Command::new("gnome-screenshot");
|
||||||
|
cmd.arg("-f").arg(&tmp_path);
|
||||||
|
if want_region {
|
||||||
|
cmd.arg("-a"); // Area
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = cmd.output().map_err(|e| format!("gnome-screenshot Fehler: {}", e))?;
|
||||||
|
|
||||||
|
if result.status.success() && tmp.exists() {
|
||||||
|
let bytes = std::fs::read(&tmp).map_err(|e| e.to_string())?;
|
||||||
|
let _ = std::fs::remove_file(&tmp);
|
||||||
|
use base64::Engine;
|
||||||
|
return Ok(base64::engine::general_purpose::STANDARD.encode(&bytes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err("Kein Screenshot-Tool gefunden. Installiere spectacle (KDE), scrot oder gnome-screenshot.".into())
|
||||||
|
}
|
||||||
|
|
||||||
// ============ Playwright-Infos ============
|
// ============ Playwright-Infos ============
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { listen } from '@tauri-apps/api/event';
|
||||||
|
|
||||||
interface Permission {
|
interface Permission {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -14,29 +15,61 @@
|
||||||
last_used: string | null;
|
last_used: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let permissions: Permission[] = [];
|
interface GuardCheck {
|
||||||
let blockedPatterns: string[] = [];
|
tool: string;
|
||||||
let loading = true;
|
command: string;
|
||||||
let showAddForm = false;
|
risk: string;
|
||||||
|
allowed: boolean;
|
||||||
|
needs_confirmation: boolean;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let permissions = $state<Permission[]>([]);
|
||||||
|
let blockedPatterns = $state<string[]>([]);
|
||||||
|
let recentChecks = $state<GuardCheck[]>([]);
|
||||||
|
let loading = $state(true);
|
||||||
|
let showAddForm = $state(false);
|
||||||
|
let activeTab = $state<'live' | 'rules' | 'blocked'>('live');
|
||||||
|
|
||||||
// Formular-State
|
// Formular-State
|
||||||
let newPattern = '';
|
let newPattern = $state('');
|
||||||
let newTool = '';
|
let newTool = $state('');
|
||||||
let newAction: 'allow' | 'deny' = 'allow';
|
let newAction = $state<'allow' | 'deny'>('allow');
|
||||||
let newType: 'session' | 'permanent' = 'permanent';
|
let newType = $state<'session' | 'permanent'>('permanent');
|
||||||
|
let testResult = $state<string | null>(null);
|
||||||
|
|
||||||
|
// Statistiken
|
||||||
|
let stats = $derived({
|
||||||
|
total: recentChecks.length,
|
||||||
|
safe: recentChecks.filter(c => c.risk === 'safe').length,
|
||||||
|
moderate: recentChecks.filter(c => c.risk === 'moderate').length,
|
||||||
|
critical: recentChecks.filter(c => c.risk === 'critical').length,
|
||||||
|
blocked: recentChecks.filter(c => c.risk === 'blocked').length,
|
||||||
|
});
|
||||||
|
|
||||||
|
let unlistenGuard: (() => void) | null = null;
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
try {
|
try {
|
||||||
permissions = await invoke('get_permissions');
|
permissions = await invoke('get_permissions');
|
||||||
blockedPatterns = await invoke('get_blocked_patterns');
|
blockedPatterns = await invoke('get_blocked_patterns');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Fehler beim Laden:', err);
|
console.error('Guard-Rails Ladefehler:', err);
|
||||||
}
|
}
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(async () => {
|
||||||
loadData();
|
await loadData();
|
||||||
|
|
||||||
|
// Live Guard-Check Events empfangen
|
||||||
|
unlistenGuard = await listen<GuardCheck>('guard-check', (event) => {
|
||||||
|
recentChecks = [event.payload, ...recentChecks.slice(0, 49)];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
unlistenGuard?.();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function addPermission() {
|
async function addPermission() {
|
||||||
|
|
@ -52,9 +85,10 @@
|
||||||
newPattern = '';
|
newPattern = '';
|
||||||
newTool = '';
|
newTool = '';
|
||||||
showAddForm = false;
|
showAddForm = false;
|
||||||
|
testResult = null;
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Fehler:', err);
|
console.error('Permission-Fehler:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,34 +97,136 @@
|
||||||
await invoke('remove_permission', { id });
|
await invoke('remove_permission', { id });
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Fehler:', err);
|
console.error('Lösch-Fehler:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function quickAllow(check: GuardCheck) {
|
||||||
|
try {
|
||||||
|
await invoke('add_permission', {
|
||||||
|
pattern: check.command,
|
||||||
|
tool: check.tool || null,
|
||||||
|
pathPattern: null,
|
||||||
|
permissionType: 'session',
|
||||||
|
action: 'allow',
|
||||||
|
});
|
||||||
|
await loadData();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Quick-Allow Fehler:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkAction() {
|
async function checkAction() {
|
||||||
if (!newPattern.trim()) return;
|
if (!newPattern.trim()) return;
|
||||||
try {
|
try {
|
||||||
const result = await invoke('check_action', {
|
const result: any = await invoke('check_action', {
|
||||||
tool: newTool || 'Bash',
|
tool: newTool || 'Bash',
|
||||||
command: newPattern,
|
command: newPattern,
|
||||||
path: null,
|
path: null,
|
||||||
});
|
});
|
||||||
console.log('Check-Ergebnis:', result);
|
testResult = `${riskEmoji(result.risk)} ${result.risk.toUpperCase()} — ${result.allowed ? 'Erlaubt' : result.needs_confirmation ? 'Bestätigung nötig' : 'Blockiert'}`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Fehler:', err);
|
testResult = `Fehler: ${err}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function riskEmoji(risk: string): string {
|
||||||
|
switch (risk) {
|
||||||
|
case 'safe': return '🟢';
|
||||||
|
case 'moderate': return '🟡';
|
||||||
|
case 'critical': return '🔴';
|
||||||
|
case 'blocked': return '⛔';
|
||||||
|
default: return '⚪';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function riskClass(risk: string): string {
|
||||||
|
switch (risk) {
|
||||||
|
case 'safe': return 'risk-safe';
|
||||||
|
case 'moderate': return 'risk-moderate';
|
||||||
|
case 'critical': return 'risk-critical';
|
||||||
|
case 'blocked': return 'risk-blocked';
|
||||||
|
default: return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(ts: number): string {
|
||||||
|
const d = new Date(ts);
|
||||||
|
return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateCmd(cmd: string, max: number = 60): string {
|
||||||
|
return cmd.length > max ? cmd.slice(0, max) + '...' : cmd;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="guard-panel">
|
<div class="guard-panel">
|
||||||
|
<!-- Header mit Statistik-Leiste -->
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h2>🛡️ Guard-Rails</h2>
|
<h2>🛡️ Guard-Rails</h2>
|
||||||
<button class="btn-add" on:click={() => showAddForm = !showAddForm}>
|
<div class="stats-bar">
|
||||||
{showAddForm ? '✕' : '+ Regel'}
|
{#if stats.total > 0}
|
||||||
|
<span class="stat safe" title="Safe">{stats.safe}</span>
|
||||||
|
<span class="stat moderate" title="Moderate">{stats.moderate}</span>
|
||||||
|
<span class="stat critical" title="Critical">{stats.critical}</span>
|
||||||
|
{#if stats.blocked > 0}
|
||||||
|
<span class="stat blocked" title="Blocked">{stats.blocked}</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab-Navigation -->
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab" class:active={activeTab === 'live'} onclick={() => activeTab = 'live'}>
|
||||||
|
Live ({recentChecks.length})
|
||||||
|
</button>
|
||||||
|
<button class="tab" class:active={activeTab === 'rules'} onclick={() => activeTab = 'rules'}>
|
||||||
|
Regeln ({permissions.length})
|
||||||
|
</button>
|
||||||
|
<button class="tab" class:active={activeTab === 'blocked'} onclick={() => activeTab = 'blocked'}>
|
||||||
|
Blockiert ({blockedPatterns.length})
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Neue Regel hinzufügen -->
|
{#if loading}
|
||||||
|
<div class="loading-state">Lade Guard-Rails...</div>
|
||||||
|
|
||||||
|
{:else if activeTab === 'live'}
|
||||||
|
<!-- Live Tool-Checks -->
|
||||||
|
<div class="live-feed">
|
||||||
|
{#if recentChecks.length === 0}
|
||||||
|
<div class="empty-hint">
|
||||||
|
<span class="empty-icon">📡</span>
|
||||||
|
<span>Warte auf Tool-Nutzung...</span>
|
||||||
|
<span class="empty-sub">Hier erscheinen Live-Checks wenn Claude Tools verwendet</span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{#each recentChecks as check, i}
|
||||||
|
<div class="check-item {riskClass(check.risk)}">
|
||||||
|
<div class="check-header">
|
||||||
|
<span class="check-risk">{riskEmoji(check.risk)}</span>
|
||||||
|
<span class="check-tool">{check.tool}</span>
|
||||||
|
<span class="check-time">{formatTime(check.timestamp)}</span>
|
||||||
|
</div>
|
||||||
|
<code class="check-cmd">{truncateCmd(check.command)}</code>
|
||||||
|
{#if check.needs_confirmation && !check.allowed}
|
||||||
|
<button class="btn-quick-allow" onclick={() => quickAllow(check)}>
|
||||||
|
✅ Erlauben (Session)
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{:else if activeTab === 'rules'}
|
||||||
|
<!-- Regeln verwalten -->
|
||||||
|
<div class="rules-section">
|
||||||
|
<button class="btn-add" onclick={() => showAddForm = !showAddForm}>
|
||||||
|
{showAddForm ? '✕ Abbrechen' : '+ Neue Regel'}
|
||||||
|
</button>
|
||||||
|
|
||||||
{#if showAddForm}
|
{#if showAddForm}
|
||||||
<div class="add-form">
|
<div class="add-form">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
|
|
@ -116,52 +252,62 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="btn-save" on:click={addPermission}>Speichern</button>
|
<button class="btn-save" onclick={addPermission}>Speichern</button>
|
||||||
<button class="btn-test" on:click={checkAction}>Testen</button>
|
<button class="btn-test" onclick={checkAction}>Testen</button>
|
||||||
</div>
|
</div>
|
||||||
|
{#if testResult}
|
||||||
|
<div class="test-result">{testResult}</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if loading}
|
|
||||||
<div class="loading-state">Lade...</div>
|
|
||||||
{:else}
|
|
||||||
<!-- Aktive Regeln -->
|
|
||||||
<div class="section">
|
|
||||||
<h3>Aktive Regeln ({permissions.length})</h3>
|
|
||||||
{#if permissions.length === 0}
|
{#if permissions.length === 0}
|
||||||
<div class="empty-hint">Keine Regeln definiert</div>
|
<div class="empty-hint">
|
||||||
|
<span>Keine Regeln definiert</span>
|
||||||
|
<span class="empty-sub">Regeln werden bei Tool-Nutzung oder manuell erstellt</span>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="rules-list">
|
<div class="rules-list">
|
||||||
{#each permissions as perm}
|
{#each permissions as perm}
|
||||||
<div class="rule-item" class:deny={perm.action === 'Deny'}>
|
<div class="rule-item" class:deny={perm.action === 'Deny'}>
|
||||||
<div class="rule-main">
|
<div class="rule-main">
|
||||||
<span class="rule-action" class:allow={perm.action === 'Allow'} class:deny={perm.action === 'Deny'}>
|
<span class="rule-action">
|
||||||
{perm.action === 'Allow' ? '✅' : '🚫'}
|
{perm.action === 'Allow' ? '✅' : '🚫'}
|
||||||
</span>
|
</span>
|
||||||
|
<div class="rule-info">
|
||||||
<code class="rule-pattern">{perm.pattern}</code>
|
<code class="rule-pattern">{perm.pattern}</code>
|
||||||
|
<div class="rule-tags">
|
||||||
{#if perm.tool}
|
{#if perm.tool}
|
||||||
<span class="rule-tool">{perm.tool}</span>
|
<span class="tag tool">{perm.tool}</span>
|
||||||
|
{/if}
|
||||||
|
<span class="tag type">
|
||||||
|
{perm.permission_type === 'Permanent' ? '💾 Dauerhaft' : '⏱️ Session'}
|
||||||
|
</span>
|
||||||
|
{#if perm.use_count > 0}
|
||||||
|
<span class="tag count">{perm.use_count}x genutzt</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="rule-meta">
|
|
||||||
<span class="rule-type">
|
|
||||||
{perm.permission_type === 'Permanent' ? '💾' : '⏱️'}
|
|
||||||
</span>
|
|
||||||
<span class="rule-count">{perm.use_count}x</span>
|
|
||||||
<button class="btn-delete" on:click={() => removePermission(perm.id)}>🗑️</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn-delete" onclick={() => removePermission(perm.id)} title="Regel löschen">
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{:else if activeTab === 'blocked'}
|
||||||
<!-- Blockierte Patterns -->
|
<!-- Blockierte Patterns -->
|
||||||
<div class="section">
|
<div class="blocked-section">
|
||||||
<h3>🚫 Immer blockiert ({blockedPatterns.length})</h3>
|
<div class="blocked-info">
|
||||||
|
Diese Befehle sind permanent blockiert und können nicht freigegeben werden.
|
||||||
|
</div>
|
||||||
<div class="blocked-list">
|
<div class="blocked-list">
|
||||||
{#each blockedPatterns as pattern}
|
{#each blockedPatterns as pattern}
|
||||||
<div class="blocked-item">
|
<div class="blocked-item">
|
||||||
|
<span class="blocked-icon">⛔</span>
|
||||||
<code>{pattern}</code>
|
<code>{pattern}</code>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
@ -183,12 +329,168 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: var(--spacing-md);
|
margin-bottom: var(--spacing-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header h2 {
|
.panel-header h2 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Statistik-Leiste */
|
||||||
|
.stats-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat.safe { background: rgba(34, 197, 94, 0.2); color: #22c55e; }
|
||||||
|
.stat.moderate { background: rgba(234, 179, 8, 0.2); color: #eab308; }
|
||||||
|
.stat.critical { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
||||||
|
.stat.blocked { background: rgba(139, 92, 246, 0.2); color: #8b5cf6; }
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover:not(.active) {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading & Empty */
|
||||||
|
.loading-state {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-hint {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-sub {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Live-Feed */
|
||||||
|
.live-feed {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-item {
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border-left: 3px solid var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-item.risk-safe { border-left-color: #22c55e; }
|
||||||
|
.check-item.risk-moderate { border-left-color: #eab308; }
|
||||||
|
.check-item.risk-critical { border-left-color: #ef4444; }
|
||||||
|
.check-item.risk-blocked { border-left-color: #8b5cf6; }
|
||||||
|
|
||||||
|
.check-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-risk {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-tool {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-time {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-cmd {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-quick-allow {
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 600;
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
color: #22c55e;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-quick-allow:hover {
|
||||||
|
background: rgba(34, 197, 94, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Regeln */
|
||||||
|
.rules-section {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-add {
|
.btn-add {
|
||||||
|
|
@ -198,18 +500,17 @@
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-add:hover {
|
.btn-add:hover {
|
||||||
background: var(--accent-hover);
|
background: var(--accent-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Formular */
|
|
||||||
.add-form {
|
.add-form {
|
||||||
padding: var(--spacing-md);
|
padding: var(--spacing-md);
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-sm);
|
gap: var(--spacing-sm);
|
||||||
|
|
@ -223,6 +524,16 @@
|
||||||
.form-row input {
|
.form-row input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-row select {
|
.form-row select {
|
||||||
|
|
@ -247,43 +558,19 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-save {
|
.btn-save { background: var(--success); color: var(--bg-primary); }
|
||||||
background: var(--success);
|
.btn-save:hover { filter: brightness(1.1); }
|
||||||
color: var(--bg-primary);
|
.btn-test { background: var(--bg-tertiary); color: var(--text-primary); }
|
||||||
}
|
.btn-test:hover { filter: brightness(1.1); }
|
||||||
|
|
||||||
.btn-test {
|
.test-result {
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
color: var(--text-primary);
|
border-radius: var(--radius-sm);
|
||||||
}
|
|
||||||
|
|
||||||
/* Sektionen */
|
|
||||||
.section {
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section h3 {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-state {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-hint {
|
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-secondary);
|
font-weight: 500;
|
||||||
text-align: center;
|
|
||||||
padding: var(--spacing-md);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Regeln */
|
|
||||||
.rules-list {
|
.rules-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -306,12 +593,24 @@
|
||||||
|
|
||||||
.rule-main {
|
.rule-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
gap: var(--spacing-sm);
|
gap: var(--spacing-sm);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rule-action {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.rule-pattern {
|
.rule-pattern {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -319,27 +618,36 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-tool {
|
.rule-tags {
|
||||||
font-size: 0.625rem;
|
display: flex;
|
||||||
padding: 1px 4px;
|
gap: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
font-size: 0.55rem;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 6px;
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-meta {
|
.tag.tool {
|
||||||
display: flex;
|
background: rgba(96, 165, 250, 0.15);
|
||||||
align-items: center;
|
color: #60a5fa;
|
||||||
gap: var(--spacing-xs);
|
}
|
||||||
font-size: 0.625rem;
|
|
||||||
color: var(--text-secondary);
|
.tag.count {
|
||||||
|
background: rgba(234, 179, 8, 0.15);
|
||||||
|
color: #eab308;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-delete {
|
.btn-delete {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
padding: 2px;
|
padding: 4px;
|
||||||
opacity: 0.5;
|
opacity: 0.4;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-delete:hover {
|
.btn-delete:hover {
|
||||||
|
|
@ -347,6 +655,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Blockiert */
|
/* Blockiert */
|
||||||
|
.blocked-section {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocked-info {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
background: rgba(239, 68, 68, 0.05);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border-left: 3px solid var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
.blocked-list {
|
.blocked-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -354,14 +678,24 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocked-item {
|
.blocked-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
padding: var(--spacing-xs) var(--spacing-sm);
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
background: rgba(239, 68, 68, 0.1);
|
background: rgba(239, 68, 68, 0.08);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocked-icon {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocked-item code {
|
.blocked-item code {
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { emit } from '@tauri-apps/api/event';
|
||||||
import IdePanel from './IdePanel.svelte';
|
import IdePanel from './IdePanel.svelte';
|
||||||
|
|
||||||
interface XvfbStatus {
|
interface XvfbStatus {
|
||||||
|
|
@ -15,7 +16,19 @@
|
||||||
hint: string;
|
hint: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let section = $state<'ide' | 'playwright' | 'dbus' | 'xvfb'>('ide');
|
interface DesktopAction {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
category: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let section = $state<'actions' | 'ide' | 'playwright' | 'dbus' | 'xvfb'>('actions');
|
||||||
|
let actions = $state<DesktopAction[]>([]);
|
||||||
|
let actionResult = $state<string | null>(null);
|
||||||
|
let screenshotPreview = $state<string | null>(null);
|
||||||
|
let screenshotCapturing = $state(false);
|
||||||
|
|
||||||
let xvfb = $state<XvfbStatus>({ running: false, display_num: 1, pid: null, resolution: '1920x1080x24' });
|
let xvfb = $state<XvfbStatus>({ running: false, display_num: 1, pid: null, resolution: '1920x1080x24' });
|
||||||
let playwright = $state<PlaywrightInfo>({ available: false, hint: '' });
|
let playwright = $state<PlaywrightInfo>({ available: false, hint: '' });
|
||||||
|
|
@ -89,14 +102,62 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadActions() {
|
||||||
|
try {
|
||||||
|
actions = await invoke<DesktopAction[]>('dbus_list_actions');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Aktionen laden:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAction(actionId: string, arg?: string) {
|
||||||
|
actionResult = null;
|
||||||
|
try {
|
||||||
|
const result = await invoke<any>('dbus_run_action', {
|
||||||
|
actionId,
|
||||||
|
arg: arg || null,
|
||||||
|
});
|
||||||
|
actionResult = result.success
|
||||||
|
? `✅ ${result.stdout || 'Erfolgreich'}`
|
||||||
|
: `❌ ${result.stderr || 'Fehler'}`;
|
||||||
|
} catch (err) {
|
||||||
|
actionResult = `❌ ${err}`;
|
||||||
|
}
|
||||||
|
// Ergebnis nach 3s ausblenden
|
||||||
|
setTimeout(() => { actionResult = null; }, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureScreenshot(region: boolean = true) {
|
||||||
|
screenshotCapturing = true;
|
||||||
|
screenshotPreview = null;
|
||||||
|
try {
|
||||||
|
const b64 = await invoke<string>('capture_screenshot', { region });
|
||||||
|
screenshotPreview = `data:image/png;base64,${b64}`;
|
||||||
|
} catch (err) {
|
||||||
|
errorMsg = String(err);
|
||||||
|
} finally {
|
||||||
|
screenshotCapturing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendScreenshotToChat() {
|
||||||
|
if (!screenshotPreview) return;
|
||||||
|
// Ans ChatPanel senden über emit
|
||||||
|
await emit('screenshot-to-chat', { image: screenshotPreview });
|
||||||
|
actionResult = '✅ Screenshot an Chat gesendet';
|
||||||
|
setTimeout(() => { actionResult = null; }, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadXvfb();
|
loadXvfb();
|
||||||
loadPlaywright();
|
loadPlaywright();
|
||||||
|
loadActions();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="programs-panel">
|
<div class="programs-panel">
|
||||||
<div class="section-tabs">
|
<div class="section-tabs">
|
||||||
|
<button class:active={section === 'actions'} onclick={() => (section = 'actions')}>⚡ Aktionen</button>
|
||||||
<button class:active={section === 'ide'} onclick={() => (section = 'ide')}>🧩 VSCodium</button>
|
<button class:active={section === 'ide'} onclick={() => (section = 'ide')}>🧩 VSCodium</button>
|
||||||
<button class:active={section === 'playwright'} onclick={() => (section = 'playwright')}>🎭 Playwright</button>
|
<button class:active={section === 'playwright'} onclick={() => (section = 'playwright')}>🎭 Playwright</button>
|
||||||
<button class:active={section === 'dbus'} onclick={() => (section = 'dbus')}>🔌 D-Bus</button>
|
<button class:active={section === 'dbus'} onclick={() => (section = 'dbus')}>🔌 D-Bus</button>
|
||||||
|
|
@ -117,7 +178,44 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="section-body">
|
<div class="section-body">
|
||||||
{#if section === 'ide'}
|
{#if section === 'actions'}
|
||||||
|
<h3>⚡ Desktop-Aktionen</h3>
|
||||||
|
{#if actionResult}
|
||||||
|
<div class="action-result">{actionResult}</div>
|
||||||
|
{/if}
|
||||||
|
<div class="actions-grid">
|
||||||
|
{#each actions as action}
|
||||||
|
<button class="action-card" onclick={() => runAction(action.id)} title={action.description}>
|
||||||
|
<span class="action-icon">{action.icon}</span>
|
||||||
|
<span class="action-name">{action.name}</span>
|
||||||
|
<span class="action-cat">{action.category}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<!-- Screenshot-Analyse -->
|
||||||
|
<h3 style="margin-top: var(--spacing-md);">📸 Screenshot-Analyse</h3>
|
||||||
|
<div class="screenshot-controls">
|
||||||
|
<button class="btn-screenshot" onclick={() => captureScreenshot(true)} disabled={screenshotCapturing}>
|
||||||
|
{screenshotCapturing ? '⏳ Auswählen...' : '📐 Bereich markieren'}
|
||||||
|
</button>
|
||||||
|
<button class="btn-screenshot secondary" onclick={() => captureScreenshot(false)} disabled={screenshotCapturing}>
|
||||||
|
🖥️ Ganzer Bildschirm
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{#if screenshotPreview}
|
||||||
|
<div class="screenshot-preview">
|
||||||
|
<img src={screenshotPreview} alt="Screenshot" />
|
||||||
|
<div class="screenshot-actions">
|
||||||
|
<button onclick={sendScreenshotToChat}>💬 An Claude senden</button>
|
||||||
|
<button onclick={() => screenshotPreview = null}>✕ Verwerfen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="hint">
|
||||||
|
Claude kann diese Aktionen auch direkt im Chat nutzen.
|
||||||
|
Screenshots werden als Bild an Claude gesendet — ideal zum UI-Debuggen.
|
||||||
|
</div>
|
||||||
|
{:else if section === 'ide'}
|
||||||
<IdePanel />
|
<IdePanel />
|
||||||
{:else if section === 'playwright'}
|
{:else if section === 'playwright'}
|
||||||
<h3>🎭 Playwright (Browser-Automation)</h3>
|
<h3>🎭 Playwright (Browser-Automation)</h3>
|
||||||
|
|
@ -315,6 +413,134 @@
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Desktop-Aktionen Grid */
|
||||||
|
.actions-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: var(--spacing-md) var(--spacing-sm);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-color: var(--accent);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-name {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-cat {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-result {
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
animation: fadeIn 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(-4px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Screenshot-Analyse */
|
||||||
|
.screenshot-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-screenshot {
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: filter 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-screenshot:hover:not(:disabled) {
|
||||||
|
filter: brightness(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-screenshot:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-screenshot.secondary {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-preview {
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-preview img {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-actions button {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-actions button:first-child {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue