From d4c57b777a564623bb45ad6eae89c6d180456e96 Mon Sep 17 00:00:00 2001 From: Eddy Date: Tue, 21 Apr 2026 12:02:24 +0200 Subject: [PATCH] feat: Guard-Rails UI, D-Bus Aktionen, Screenshot-Analyse [appimage] 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 --- CHANGELOG.md | 3 + ROADMAP.md | 6 +- src-tauri/src/guard.rs | 38 +- src-tauri/src/lib.rs | 3 + src-tauri/src/programs.rs | 231 +++++++++ src/lib/components/GuardRailsPanel.svelte | 574 +++++++++++++++++----- src/lib/components/ProgramsPanel.svelte | 230 ++++++++- 7 files changed, 954 insertions(+), 131 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d768de..94a83ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/). ## [Unreleased] - 2026-04-21 ### 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`) - **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`) diff --git a/ROADMAP.md b/ROADMAP.md index 48e5774..cf52cf9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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) | | 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 | | ✅ 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 | @@ -91,10 +91,10 @@ Alles aus Phase 1-16 ist implementiert und funktionsfaehig: | 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) | | ✅ 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 | --- diff --git a/src-tauri/src/guard.rs b/src-tauri/src/guard.rs index a8775c5..38093a0 100644 --- a/src-tauri/src/guard.rs +++ b/src-tauri/src/guard.rs @@ -2,7 +2,7 @@ // Risiko-Klassifikation und Freigabe-Management use serde::{Deserialize, Serialize}; -use tauri::{AppHandle, Manager}; +use tauri::{AppHandle, Emitter, Manager}; /// Risiko-Level einer Aktion #[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 ts = chrono::Utc::now().timestamp_millis(); + // Wenn blockiert, sofort ablehnen 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!({ "allowed": false, "risk": "blocked", @@ -365,9 +371,15 @@ pub async fn check_action( // Permission prüfen 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!({ - "allowed": perm.action == PermissionAction::Allow, - "risk": format!("{:?}", risk).to_lowercase(), + "allowed": perm_allowed, + "risk": risk_str, "matched_rule": perm.id })); } @@ -375,10 +387,24 @@ pub async fn check_action( // Kein Match - Frontend muss fragen 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!({ - "allowed": risk == RiskLevel::Safe, - "risk": format!("{:?}", risk).to_lowercase(), - "needs_confirmation": risk != RiskLevel::Safe, + "allowed": allowed, + "risk": risk_str, + "needs_confirmation": needs_confirm, "suggested_pattern": suggested })) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f74831b..61f530d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -155,6 +155,9 @@ pub fn run() { // Programm-Steuerung (D-Bus, Xvfb, Playwright-Info) programs::dbus_call, programs::dbus_list_services, + programs::dbus_list_actions, + programs::dbus_run_action, + programs::capture_screenshot, programs::xvfb_start, programs::xvfb_stop, programs::xvfb_status, diff --git a/src-tauri/src/programs.rs b/src-tauri/src/programs.rs index e73f940..c367fb1 100644 --- a/src-tauri/src/programs.rs +++ b/src-tauri/src/programs.rs @@ -95,6 +95,157 @@ pub async fn dbus_list_services(session: Option) -> Result, St 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, 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, +) -> Result { + 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 { + // 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) ============ #[derive(Debug, Clone, Serialize, Deserialize)] @@ -219,6 +370,86 @@ pub async fn xvfb_screenshot(display_num: Option) -> Result )) } +// ============ Screenshot-Analyse ============ + +/// Macht einen Screenshot (Bereich oder Vollbild) und gibt Base64-PNG zurueck +#[tauri::command] +pub async fn capture_screenshot( + region: Option, +) -> Result { + 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 ============ #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/lib/components/GuardRailsPanel.svelte b/src/lib/components/GuardRailsPanel.svelte index 1346612..c94073e 100644 --- a/src/lib/components/GuardRailsPanel.svelte +++ b/src/lib/components/GuardRailsPanel.svelte @@ -1,6 +1,7 @@
+

🛡️ Guard-Rails

-
+ + +
+ + +
- - {#if showAddForm} -
-
- -
-
- - - -
-
- - -
-
- {/if} - {#if loading} -
Lade...
- {:else} - -
-

Aktive Regeln ({permissions.length})

+
Lade Guard-Rails...
+ + {:else if activeTab === 'live'} + +
+ {#if recentChecks.length === 0} +
+ 📡 + Warte auf Tool-Nutzung... + Hier erscheinen Live-Checks wenn Claude Tools verwendet +
+ {:else} + {#each recentChecks as check, i} +
+
+ {riskEmoji(check.risk)} + {check.tool} + {formatTime(check.timestamp)} +
+ {truncateCmd(check.command)} + {#if check.needs_confirmation && !check.allowed} + + {/if} +
+ {/each} + {/if} +
+ + {:else if activeTab === 'rules'} + +
+ + + {#if showAddForm} +
+
+ +
+
+ + + +
+
+ + +
+ {#if testResult} +
{testResult}
+ {/if} +
+ {/if} + {#if permissions.length === 0} -
Keine Regeln definiert
+
+ Keine Regeln definiert + Regeln werden bei Tool-Nutzung oder manuell erstellt +
{:else}
{#each permissions as perm}
- + {perm.action === 'Allow' ? '✅' : '🚫'} - {perm.pattern} - {#if perm.tool} - {perm.tool} - {/if} -
-
- - {perm.permission_type === 'Permanent' ? '💾' : '⏱️'} - - {perm.use_count}x - +
+ {perm.pattern} +
+ {#if perm.tool} + {perm.tool} + {/if} + + {perm.permission_type === 'Permanent' ? '💾 Dauerhaft' : '⏱️ Session'} + + {#if perm.use_count > 0} + {perm.use_count}x genutzt + {/if} +
+
+
{/each}
{/if}
+ {:else if activeTab === 'blocked'} -
-

🚫 Immer blockiert ({blockedPatterns.length})

+
+
+ Diese Befehle sind permanent blockiert und können nicht freigegeben werden. +
{#each blockedPatterns as pattern}
+ {pattern}
{/each} @@ -183,12 +329,168 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: var(--spacing-md); + margin-bottom: var(--spacing-sm); } .panel-header h2 { font-size: 1rem; 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 { @@ -198,18 +500,17 @@ border-radius: var(--radius-sm); font-size: 0.75rem; font-weight: 600; + align-self: flex-start; } .btn-add:hover { background: var(--accent-hover); } - /* Formular */ .add-form { padding: var(--spacing-md); background: var(--bg-secondary); border-radius: var(--radius-md); - margin-bottom: var(--spacing-md); display: flex; flex-direction: column; gap: var(--spacing-sm); @@ -223,6 +524,16 @@ .form-row input { flex: 1; 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 { @@ -247,43 +558,19 @@ font-weight: 600; } - .btn-save { - background: var(--success); - color: var(--bg-primary); - } + .btn-save { background: var(--success); color: var(--bg-primary); } + .btn-save:hover { filter: brightness(1.1); } + .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); - color: var(--text-primary); - } - - /* 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 { + border-radius: var(--radius-sm); font-size: 0.75rem; - color: var(--text-secondary); - text-align: center; - padding: var(--spacing-md); + font-weight: 500; } - /* Regeln */ .rules-list { display: flex; flex-direction: column; @@ -306,12 +593,24 @@ .rule-main { display: flex; - align-items: center; + align-items: flex-start; gap: var(--spacing-sm); flex: 1; 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 { font-size: 0.75rem; overflow: hidden; @@ -319,27 +618,36 @@ white-space: nowrap; } - .rule-tool { - font-size: 0.625rem; - padding: 1px 4px; + .rule-tags { + display: flex; + gap: 4px; + flex-wrap: wrap; + } + + .tag { + font-size: 0.55rem; + padding: 1px 5px; + border-radius: 6px; background: var(--bg-tertiary); - border-radius: var(--radius-sm); color: var(--text-secondary); } - .rule-meta { - display: flex; - align-items: center; - gap: var(--spacing-xs); - font-size: 0.625rem; - color: var(--text-secondary); + .tag.tool { + background: rgba(96, 165, 250, 0.15); + color: #60a5fa; + } + + .tag.count { + background: rgba(234, 179, 8, 0.15); + color: #eab308; } .btn-delete { font-size: 0.75rem; - padding: 2px; - opacity: 0.5; + padding: 4px; + opacity: 0.4; transition: opacity 0.2s; + flex-shrink: 0; } .btn-delete:hover { @@ -347,6 +655,22 @@ } /* 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 { display: flex; flex-direction: column; @@ -354,14 +678,24 @@ } .blocked-item { + display: flex; + align-items: center; + gap: 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); + } + + .blocked-icon { font-size: 0.7rem; + flex-shrink: 0; } .blocked-item code { color: var(--error); font-size: 0.7rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } diff --git a/src/lib/components/ProgramsPanel.svelte b/src/lib/components/ProgramsPanel.svelte index 9231462..02d9360 100644 --- a/src/lib/components/ProgramsPanel.svelte +++ b/src/lib/components/ProgramsPanel.svelte @@ -1,6 +1,7 @@
+ @@ -117,7 +178,44 @@ {/if}
- {#if section === 'ide'} + {#if section === 'actions'} +

⚡ Desktop-Aktionen

+ {#if actionResult} +
{actionResult}
+ {/if} +
+ {#each actions as action} + + {/each} +
+ +

📸 Screenshot-Analyse

+
+ + +
+ {#if screenshotPreview} +
+ Screenshot +
+ + +
+
+ {/if} +
+ Claude kann diese Aktionen auch direkt im Chat nutzen. + Screenshots werden als Bild an Claude gesendet — ideal zum UI-Debuggen. +
+ {:else if section === 'ide'} {:else if section === 'playwright'}

🎭 Playwright (Browser-Automation)

@@ -315,6 +413,134 @@ 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 { color: var(--text-secondary); font-size: 0.8rem;