Compare commits
No commits in common. "3e5021dbf0f2f3e888d7f14e4459201c9999204f" and "5003fb9996f54bddedac80f8abb94aa66bdd2c11" have entirely different histories.
3e5021dbf0
...
5003fb9996
22 changed files with 232 additions and 2822 deletions
|
|
@ -24,8 +24,6 @@
|
|||
"dependencies": {
|
||||
"@anthropic-ai/claude-code": "^0.2.0",
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0",
|
||||
"marked": "^18.0.0",
|
||||
"paneforge": "^1.0.2"
|
||||
"@tauri-apps/plugin-shell": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,324 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
// Claude Desktop — Bridge zwischen Tauri-Backend und Claude CLI
|
||||
//
|
||||
// Kommunikation: stdin/stdout als NDJSON (eine JSON-Zeile pro Nachricht)
|
||||
//
|
||||
// Eingehend (von Tauri):
|
||||
// { "command": "message", "id": "req-1", "message": "Fixe den Bug..." }
|
||||
// { "command": "stop", "id": "req-2" }
|
||||
//
|
||||
// Ausgehend (an Tauri):
|
||||
// { "type": "event", "event": "ready" }
|
||||
// { "type": "event", "event": "agent-started", "payload": { "id": "...", "type": "Main" } }
|
||||
// { "type": "event", "event": "text", "payload": { "text": "..." } }
|
||||
// { "type": "event", "event": "tool-start", "payload": { "id": "...", "tool": "Read", "input": {...} } }
|
||||
// { "type": "event", "event": "tool-end", "payload": { "id": "...", "success": true } }
|
||||
// { "type": "event", "event": "result", "payload": { "cost": 0.01, "tokens": {...} } }
|
||||
// { "type": "event", "event": "agent-stopped", "payload": { "id": "..." } }
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import { createInterface } from 'node:readline';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
// Aktive Claude-Prozesse
|
||||
const activeProcesses = new Map();
|
||||
|
||||
// Session-ID für --resume
|
||||
let currentSessionId = null;
|
||||
|
||||
// ============ Kommunikation mit Tauri ============
|
||||
|
||||
function sendToTauri(msg) {
|
||||
process.stdout.write(JSON.stringify(msg) + '\n');
|
||||
}
|
||||
|
||||
function sendEvent(event, payload = {}) {
|
||||
sendToTauri({ type: 'event', event, payload });
|
||||
}
|
||||
|
||||
function sendResponse(id, result) {
|
||||
sendToTauri({ type: 'response', id, result });
|
||||
}
|
||||
|
||||
function sendError(id, error) {
|
||||
sendToTauri({ type: 'response', id, error });
|
||||
}
|
||||
|
||||
// ============ Claude CLI aufrufen ============
|
||||
|
||||
function spawnClaude(message, requestId) {
|
||||
const agentId = randomUUID();
|
||||
const args = [
|
||||
'-p', // Print-Modus (nicht interaktiv)
|
||||
'--output-format', 'stream-json', // Streaming JSON Events
|
||||
'--verbose', // Pflicht für stream-json
|
||||
];
|
||||
|
||||
// Bei Fortsetzung letzte Session verwenden
|
||||
if (currentSessionId) {
|
||||
args.push('--resume', currentSessionId);
|
||||
}
|
||||
|
||||
// Nachricht als Argument
|
||||
args.push(message);
|
||||
|
||||
// Claude CLI Pfad — npx oder global installiert
|
||||
const claudePath = process.env.CLAUDE_PATH || 'claude';
|
||||
|
||||
const proc = spawn(claudePath, args, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env: {
|
||||
...process.env,
|
||||
// Keine interaktive UI
|
||||
CLAUDE_CODE_HEADLESS: '1',
|
||||
},
|
||||
});
|
||||
|
||||
activeProcesses.set(agentId, { proc, requestId });
|
||||
|
||||
sendEvent('agent-started', {
|
||||
id: agentId,
|
||||
type: 'Main',
|
||||
task: message.substring(0, 100),
|
||||
});
|
||||
|
||||
// NDJSON-Zeilen von stdout parsen
|
||||
let fullText = '';
|
||||
let toolCounter = 0;
|
||||
|
||||
const rl = createInterface({ input: proc.stdout });
|
||||
|
||||
rl.on('line', (line) => {
|
||||
if (!line.trim()) return;
|
||||
|
||||
try {
|
||||
const event = JSON.parse(line);
|
||||
handleClaudeEvent(event, agentId, requestId, { fullText: () => fullText, addText: (t) => { fullText += t; } });
|
||||
} catch {
|
||||
// Nicht-JSON Zeile — als Text weiterleiten
|
||||
fullText += line;
|
||||
sendEvent('text', { text: line });
|
||||
}
|
||||
});
|
||||
|
||||
// stderr für Debug-Infos
|
||||
const stderrRl = createInterface({ input: proc.stderr });
|
||||
stderrRl.on('line', (line) => {
|
||||
if (process.env.CLAUDE_DEBUG) {
|
||||
process.stderr.write(`[claude-stderr] ${line}\n`);
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('close', (code) => {
|
||||
activeProcesses.delete(agentId);
|
||||
|
||||
sendEvent('agent-stopped', {
|
||||
id: agentId,
|
||||
code,
|
||||
});
|
||||
|
||||
if (activeProcesses.size === 0) {
|
||||
sendEvent('all-stopped');
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('error', (err) => {
|
||||
sendError(requestId, `Claude konnte nicht gestartet werden: ${err.message}`);
|
||||
activeProcesses.delete(agentId);
|
||||
});
|
||||
|
||||
return agentId;
|
||||
}
|
||||
|
||||
// ============ Claude Stream-JSON Events verarbeiten ============
|
||||
|
||||
function handleClaudeEvent(event, agentId, requestId, textState) {
|
||||
// Das stream-json Format hat verschiedene Event-Typen:
|
||||
// { "type": "assistant", "message": { "content": [...], ... } }
|
||||
// { "type": "content_block_start", "content_block": { "type": "text", "text": "..." } }
|
||||
// { "type": "content_block_delta", "delta": { "type": "text_delta", "text": "..." } }
|
||||
// { "type": "content_block_start", "content_block": { "type": "tool_use", "name": "Read", ... } }
|
||||
// { "type": "content_block_delta", "delta": { "type": "input_json_delta", ... } }
|
||||
// { "type": "result", "result": "...", "cost_usd": 0.01, ... }
|
||||
// { "type": "system", "subtype": "session_id", "session_id": "..." }
|
||||
|
||||
const type = event.type;
|
||||
|
||||
switch (type) {
|
||||
case 'system':
|
||||
if (event.session_id) {
|
||||
currentSessionId = event.session_id;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'rate_limit_event':
|
||||
// Ignorieren
|
||||
break;
|
||||
|
||||
case 'assistant':
|
||||
// Vollständige Nachricht — Inhalt extrahieren
|
||||
if (event.message?.content) {
|
||||
for (const block of event.message.content) {
|
||||
if (block.type === 'text') {
|
||||
sendEvent('text', { text: block.text });
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'content_block_start':
|
||||
if (event.content_block?.type === 'text') {
|
||||
// Text-Block startet
|
||||
if (event.content_block.text) {
|
||||
sendEvent('text', { text: event.content_block.text });
|
||||
}
|
||||
} else if (event.content_block?.type === 'tool_use') {
|
||||
// Tool-Aufruf startet
|
||||
sendEvent('tool-start', {
|
||||
id: event.content_block.id || randomUUID(),
|
||||
tool: event.content_block.name,
|
||||
input: event.content_block.input || {},
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'content_block_delta':
|
||||
if (event.delta?.type === 'text_delta' && event.delta.text) {
|
||||
sendEvent('text', { text: event.delta.text });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'content_block_stop':
|
||||
// Block fertig — wenn es ein Tool war, Tool-Ende senden
|
||||
break;
|
||||
|
||||
case 'tool_result':
|
||||
case 'tool_use_result':
|
||||
sendEvent('tool-end', {
|
||||
id: event.tool_use_id || event.id || '',
|
||||
success: !event.is_error,
|
||||
output: typeof event.content === 'string'
|
||||
? event.content
|
||||
: JSON.stringify(event.content)?.substring(0, 500),
|
||||
});
|
||||
break;
|
||||
|
||||
case 'result':
|
||||
// Endergebnis mit Kosten
|
||||
sendEvent('result', {
|
||||
text: event.result || '',
|
||||
cost: event.total_cost_usd || event.cost_usd || 0,
|
||||
tokens: {
|
||||
input: event.usage?.input_tokens || 0,
|
||||
output: event.usage?.output_tokens || 0,
|
||||
cache_read: event.usage?.cache_read_input_tokens || 0,
|
||||
cache_create: event.usage?.cache_creation_input_tokens || 0,
|
||||
},
|
||||
session_id: event.session_id || currentSessionId,
|
||||
duration_ms: event.duration_ms || 0,
|
||||
num_turns: event.num_turns || 0,
|
||||
});
|
||||
|
||||
// Session-ID für Fortsetzung merken
|
||||
if (event.session_id) {
|
||||
currentSessionId = event.session_id;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Unbekannte Events durchreichen
|
||||
if (event.subagent_id || event.agent_id) {
|
||||
// Sub-Agent Events
|
||||
sendEvent('subagent-event', {
|
||||
agentId: event.subagent_id || event.agent_id,
|
||||
type,
|
||||
data: event,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Alle Prozesse stoppen ============
|
||||
|
||||
function stopAll() {
|
||||
for (const [agentId, { proc }] of activeProcesses) {
|
||||
try {
|
||||
proc.kill('SIGTERM');
|
||||
// Nach 2 Sekunden hart killen
|
||||
setTimeout(() => {
|
||||
try { proc.kill('SIGKILL'); } catch {}
|
||||
}, 2000);
|
||||
} catch {}
|
||||
}
|
||||
activeProcesses.clear();
|
||||
sendEvent('all-stopped');
|
||||
}
|
||||
|
||||
// ============ Eingehende Befehle verarbeiten ============
|
||||
|
||||
function handleCommand(msg) {
|
||||
switch (msg.command) {
|
||||
case 'message':
|
||||
if (!msg.message) {
|
||||
sendError(msg.id, 'Keine Nachricht angegeben');
|
||||
return;
|
||||
}
|
||||
const agentId = spawnClaude(msg.message, msg.id);
|
||||
sendResponse(msg.id, { agentId, status: 'gestartet' });
|
||||
break;
|
||||
|
||||
case 'stop':
|
||||
stopAll();
|
||||
sendResponse(msg.id, { status: 'gestoppt' });
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
const agents = [];
|
||||
for (const [id, { proc }] of activeProcesses) {
|
||||
agents.push({
|
||||
id,
|
||||
running: !proc.killed,
|
||||
pid: proc.pid,
|
||||
});
|
||||
}
|
||||
sendResponse(msg.id, { agents, sessionId: currentSessionId });
|
||||
break;
|
||||
|
||||
case 'ping':
|
||||
sendResponse(msg.id, { pong: true });
|
||||
break;
|
||||
|
||||
default:
|
||||
sendError(msg.id, `Unbekannter Befehl: ${msg.command}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Main ============
|
||||
|
||||
// stdin zeilenweise lesen
|
||||
const rl = createInterface({ input: process.stdin });
|
||||
|
||||
rl.on('line', (line) => {
|
||||
if (!line.trim()) return;
|
||||
try {
|
||||
const msg = JSON.parse(line);
|
||||
handleCommand(msg);
|
||||
} catch (err) {
|
||||
process.stderr.write(`Ungültige Eingabe: ${err.message}\n`);
|
||||
}
|
||||
});
|
||||
|
||||
// Sauber beenden
|
||||
process.on('SIGTERM', () => {
|
||||
stopAll();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
stopAll();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Bereit-Signal senden
|
||||
sendEvent('ready', { version: '0.1.0', pid: process.pid });
|
||||
10
shell.nix
10
shell.nix
|
|
@ -2,11 +2,7 @@
|
|||
|
||||
pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
# Rust Toolchain aus nixpkgs
|
||||
rustc
|
||||
cargo
|
||||
rustfmt
|
||||
clippy
|
||||
# Rust (wir nutzen rustup, aber brauchen den Linker)
|
||||
gcc
|
||||
pkg-config
|
||||
openssl
|
||||
|
|
@ -60,9 +56,11 @@ pkgs.mkShell {
|
|||
pkgs.openssl
|
||||
]}:$LD_LIBRARY_PATH"
|
||||
|
||||
# Rust von rustup laden
|
||||
source "$HOME/.cargo/env" 2>/dev/null || true
|
||||
|
||||
echo "🦀 Claude Desktop Entwicklungsumgebung geladen"
|
||||
echo " Rust: $(rustc --version 2>/dev/null || echo 'nicht gefunden')"
|
||||
echo " Cargo: $(cargo --version 2>/dev/null || echo 'nicht gefunden')"
|
||||
echo " Node: $(node --version 2>/dev/null || echo 'nicht gefunden')"
|
||||
'';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
|||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["tray-icon"] }
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-shell = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2/capability",
|
||||
"identifier": "default",
|
||||
"description": "Claude Desktop Standardberechtigungen",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"shell:allow-open",
|
||||
"shell:allow-execute",
|
||||
"shell:allow-spawn",
|
||||
"shell:allow-stdin-write",
|
||||
"shell:allow-kill"
|
||||
]
|
||||
}
|
||||
|
|
@ -2,10 +2,7 @@
|
|||
// Protokolliert alle Änderungen an Einstellungen, Guard-Rails, Hooks, Skills, etc.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
use crate::db;
|
||||
use tauri::AppHandle;
|
||||
|
||||
/// Kategorie der Änderung
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -108,17 +105,17 @@ pub struct AuditStats {
|
|||
|
||||
/// Holt die letzten Audit-Einträge
|
||||
#[tauri::command]
|
||||
pub async fn get_audit_log(app: AppHandle, limit: Option<usize>) -> Result<Vec<AuditEntry>, String> {
|
||||
pub async fn get_audit_log(limit: Option<usize>) -> Result<Vec<AuditEntry>, String> {
|
||||
let limit = limit.unwrap_or(50);
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db_lock = state.lock().unwrap();
|
||||
db_lock.load_audit_log(limit).map_err(|e| e.to_string())
|
||||
|
||||
// TODO: Aus SQLite laden
|
||||
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
/// Fügt einen Audit-Eintrag hinzu
|
||||
#[tauri::command]
|
||||
pub async fn add_audit_entry(
|
||||
app: AppHandle,
|
||||
category: String,
|
||||
action: String,
|
||||
item_id: String,
|
||||
|
|
@ -162,20 +159,21 @@ pub async fn add_audit_entry(
|
|||
session_id: None,
|
||||
};
|
||||
|
||||
// In SQLite speichern
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db_lock = state.lock().unwrap();
|
||||
db_lock.save_audit_entry(&entry).map_err(|e| e.to_string())?;
|
||||
// TODO: In SQLite speichern
|
||||
|
||||
println!("📋 Audit: {:?} {:?} - {}", entry.action, entry.category, entry.item_name);
|
||||
println!("📋 Audit-Eintrag hinzugefügt: {:?}", entry);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Holt Audit-Statistiken
|
||||
#[tauri::command]
|
||||
pub async fn get_audit_stats(app: AppHandle) -> Result<AuditStats, String> {
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db_lock = state.lock().unwrap();
|
||||
db_lock.audit_stats().map_err(|e| e.to_string())
|
||||
pub async fn get_audit_stats() -> Result<AuditStats, String> {
|
||||
// TODO: Echte Implementierung
|
||||
|
||||
Ok(AuditStats {
|
||||
total: 0,
|
||||
auto_corrected: 0,
|
||||
today: 0,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ use std::process::{Command, Stdio};
|
|||
use std::sync::{Arc, Mutex};
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
|
||||
use crate::db;
|
||||
|
||||
/// Status eines Agents
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentStatus {
|
||||
|
|
@ -178,30 +176,6 @@ fn handle_bridge_message(app: &AppHandle, msg: BridgeMessage) {
|
|||
let _ = app.emit("claude-text", &payload);
|
||||
}
|
||||
"result" => {
|
||||
// Session-ID aus Result extrahieren und in DB speichern
|
||||
if let Some(sid) = payload.get("session_id").and_then(|v| v.as_str()) {
|
||||
if let Some(db_state) = app.try_state::<Arc<Mutex<db::Database>>>() {
|
||||
let db_lock = db_state.lock().unwrap();
|
||||
if let Ok(Some(active_id)) = db_lock.get_setting("active_session_id") {
|
||||
if !active_id.is_empty() {
|
||||
if let Ok(Some(mut session)) = db_lock.get_session(&active_id) {
|
||||
session.claude_session_id = Some(sid.to_string());
|
||||
session.message_count += 1;
|
||||
if let Some(cost) = payload.get("cost").and_then(|v| v.as_f64()) {
|
||||
session.cost_usd += cost;
|
||||
}
|
||||
if let Some(tin) = payload.get("tokens").and_then(|t| t.get("input")).and_then(|v| v.as_i64()) {
|
||||
session.token_input += tin;
|
||||
}
|
||||
if let Some(tout) = payload.get("tokens").and_then(|t| t.get("output")).and_then(|v| v.as_i64()) {
|
||||
session.token_output += tout;
|
||||
}
|
||||
let _ = db_lock.update_session(&session);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = app.emit("claude-result", &payload);
|
||||
}
|
||||
"all-stopped" => {
|
||||
|
|
|
|||
|
|
@ -1,606 +0,0 @@
|
|||
// Claude Desktop — SQLite Datenbankschicht
|
||||
// Persistiert Guard-Rails, Audit-Log, Memory und Einstellungen
|
||||
|
||||
use rusqlite::{params, Connection, Result as SqlResult};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
use crate::audit::{AuditAction, AuditCategory, AuditEntry, AuditStats};
|
||||
use crate::guard::{Permission, PermissionAction, PermissionType};
|
||||
use crate::memory::{ContextCategory, MemoryEntry, Pattern};
|
||||
|
||||
/// Eine Claude-Session
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Session {
|
||||
pub id: String,
|
||||
pub claude_session_id: Option<String>,
|
||||
pub title: String,
|
||||
pub working_dir: Option<String>,
|
||||
pub message_count: i64,
|
||||
pub token_input: i64,
|
||||
pub token_output: i64,
|
||||
pub cost_usd: f64,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub last_message: Option<String>,
|
||||
}
|
||||
|
||||
/// Datenbank-Wrapper
|
||||
pub struct Database {
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
/// Datenbank-Statistiken
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct DbStats {
|
||||
pub permissions: usize,
|
||||
pub audit_entries: usize,
|
||||
pub memory_entries: usize,
|
||||
pub patterns: usize,
|
||||
pub db_size_kb: u64,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
/// Öffnet oder erstellt die Datenbank
|
||||
pub fn open(path: &Path) -> SqlResult<Self> {
|
||||
let conn = Connection::open(path)?;
|
||||
|
||||
// WAL-Modus für bessere Performance
|
||||
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?;
|
||||
|
||||
let db = Self { conn };
|
||||
db.create_tables()?;
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
/// Schema erstellen
|
||||
fn create_tables(&self) -> SqlResult<()> {
|
||||
self.conn.execute_batch(
|
||||
"
|
||||
-- Guard-Rails Permissions
|
||||
CREATE TABLE IF NOT EXISTS permissions (
|
||||
id TEXT PRIMARY KEY,
|
||||
pattern TEXT NOT NULL,
|
||||
tool TEXT,
|
||||
path_pattern TEXT,
|
||||
action TEXT NOT NULL DEFAULT 'allow',
|
||||
created_at TEXT NOT NULL,
|
||||
use_count INTEGER DEFAULT 0,
|
||||
last_used TEXT
|
||||
);
|
||||
|
||||
-- Audit-Log
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id TEXT PRIMARY KEY,
|
||||
timestamp TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
item_id TEXT NOT NULL,
|
||||
item_name TEXT NOT NULL,
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
reason TEXT,
|
||||
auto_corrected INTEGER DEFAULT 0,
|
||||
session_id TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_category ON audit_log(category);
|
||||
|
||||
-- Memory-Einträge
|
||||
CREATE TABLE IF NOT EXISTS memory (
|
||||
id TEXT PRIMARY KEY,
|
||||
category TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
sticky INTEGER DEFAULT 0,
|
||||
auto_load INTEGER DEFAULT 0,
|
||||
last_used TEXT,
|
||||
use_count INTEGER DEFAULT 0
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_memory_category ON memory(category);
|
||||
CREATE INDEX IF NOT EXISTS idx_memory_sticky ON memory(sticky) WHERE sticky = 1;
|
||||
|
||||
-- Patterns (Vorgehensweisen)
|
||||
CREATE TABLE IF NOT EXISTS patterns (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
trigger_text TEXT,
|
||||
old_approach TEXT,
|
||||
new_approach TEXT,
|
||||
reason TEXT,
|
||||
occurrence_count INTEGER DEFAULT 1,
|
||||
auto_corrected INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Sessions (Claude-Konversationen)
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
claude_session_id TEXT,
|
||||
title TEXT NOT NULL,
|
||||
working_dir TEXT,
|
||||
message_count INTEGER DEFAULT 0,
|
||||
token_input INTEGER DEFAULT 0,
|
||||
token_output INTEGER DEFAULT 0,
|
||||
cost_usd REAL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
last_message TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_updated ON sessions(updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
||||
|
||||
-- Einstellungen (Key-Value)
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============ Permissions ============
|
||||
|
||||
/// Speichert eine Permission
|
||||
pub fn save_permission(&self, perm: &Permission) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
"INSERT OR REPLACE INTO permissions (id, pattern, tool, path_pattern, action, created_at, use_count, last_used)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
params![
|
||||
perm.id,
|
||||
perm.pattern,
|
||||
perm.tool,
|
||||
perm.path_pattern,
|
||||
format!("{:?}", perm.action).to_lowercase(),
|
||||
perm.created_at,
|
||||
perm.use_count,
|
||||
perm.last_used,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lädt alle permanenten Permissions
|
||||
pub fn load_permissions(&self) -> SqlResult<Vec<Permission>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, pattern, tool, path_pattern, action, created_at, use_count, last_used FROM permissions"
|
||||
)?;
|
||||
|
||||
let perms = stmt.query_map([], |row| {
|
||||
let action_str: String = row.get(4)?;
|
||||
let action = match action_str.as_str() {
|
||||
"deny" => PermissionAction::Deny,
|
||||
_ => PermissionAction::Allow,
|
||||
};
|
||||
|
||||
Ok(Permission {
|
||||
id: row.get(0)?,
|
||||
pattern: row.get(1)?,
|
||||
tool: row.get(2)?,
|
||||
path_pattern: row.get(3)?,
|
||||
permission_type: PermissionType::Permanent,
|
||||
action,
|
||||
created_at: row.get(5)?,
|
||||
use_count: row.get(6)?,
|
||||
last_used: row.get(7)?,
|
||||
})
|
||||
})?.collect::<SqlResult<Vec<_>>>()?;
|
||||
|
||||
Ok(perms)
|
||||
}
|
||||
|
||||
/// Löscht eine Permission
|
||||
pub fn delete_permission(&self, id: &str) -> SqlResult<()> {
|
||||
self.conn.execute("DELETE FROM permissions WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============ Audit-Log ============
|
||||
|
||||
/// Speichert einen Audit-Eintrag
|
||||
pub fn save_audit_entry(&self, entry: &AuditEntry) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
"INSERT INTO audit_log (id, timestamp, category, action, item_id, item_name, old_value, new_value, reason, auto_corrected, session_id)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
|
||||
params![
|
||||
entry.id,
|
||||
entry.timestamp,
|
||||
format!("{:?}", entry.category).to_lowercase(),
|
||||
format!("{:?}", entry.action).to_lowercase(),
|
||||
entry.item_id,
|
||||
entry.item_name,
|
||||
entry.old_value.as_ref().map(|v| v.to_string()),
|
||||
entry.new_value.as_ref().map(|v| v.to_string()),
|
||||
entry.reason,
|
||||
entry.auto_corrected as i32,
|
||||
entry.session_id,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lädt die letzten N Audit-Einträge
|
||||
pub fn load_audit_log(&self, limit: usize) -> SqlResult<Vec<AuditEntry>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, timestamp, category, action, item_id, item_name, old_value, new_value, reason, auto_corrected, session_id
|
||||
FROM audit_log ORDER BY timestamp DESC LIMIT ?1"
|
||||
)?;
|
||||
|
||||
let entries = stmt.query_map(params![limit as i64], |row| {
|
||||
let cat_str: String = row.get(2)?;
|
||||
let act_str: String = row.get(3)?;
|
||||
let old_val: Option<String> = row.get(6)?;
|
||||
let new_val: Option<String> = row.get(7)?;
|
||||
let auto_corr: i32 = row.get(9)?;
|
||||
|
||||
Ok(AuditEntry {
|
||||
id: row.get(0)?,
|
||||
timestamp: row.get(1)?,
|
||||
category: parse_audit_category(&cat_str),
|
||||
action: parse_audit_action(&act_str),
|
||||
item_id: row.get(4)?,
|
||||
item_name: row.get(5)?,
|
||||
old_value: old_val.and_then(|s| serde_json::from_str(&s).ok()),
|
||||
new_value: new_val.and_then(|s| serde_json::from_str(&s).ok()),
|
||||
reason: row.get(8)?,
|
||||
auto_corrected: auto_corr != 0,
|
||||
session_id: row.get(10)?,
|
||||
})
|
||||
})?.collect::<SqlResult<Vec<_>>>()?;
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// Audit-Statistiken
|
||||
pub fn audit_stats(&self) -> SqlResult<AuditStats> {
|
||||
let total: usize = self.conn.query_row(
|
||||
"SELECT COUNT(*) FROM audit_log", [], |row| row.get(0)
|
||||
)?;
|
||||
let auto_corrected: usize = self.conn.query_row(
|
||||
"SELECT COUNT(*) FROM audit_log WHERE auto_corrected = 1", [], |row| row.get(0)
|
||||
)?;
|
||||
let today: usize = self.conn.query_row(
|
||||
"SELECT COUNT(*) FROM audit_log WHERE timestamp LIKE ?1 || '%'",
|
||||
params![chrono::Local::now().format("%Y-%m-%d").to_string()],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
Ok(AuditStats { total, auto_corrected, today })
|
||||
}
|
||||
|
||||
// ============ Memory ============
|
||||
|
||||
/// Speichert einen Memory-Eintrag
|
||||
pub fn save_memory_entry(&self, entry: &MemoryEntry) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
"INSERT OR REPLACE INTO memory (id, category, key, value, sticky, auto_load, last_used, use_count)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
params![
|
||||
entry.id,
|
||||
format!("{:?}", entry.category),
|
||||
entry.key,
|
||||
entry.value.to_string(),
|
||||
entry.sticky as i32,
|
||||
entry.auto_load as i32,
|
||||
entry.last_used,
|
||||
entry.use_count,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lädt alle Memory-Einträge
|
||||
pub fn load_memory_entries(&self) -> SqlResult<Vec<MemoryEntry>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, category, key, value, sticky, auto_load, last_used, use_count FROM memory"
|
||||
)?;
|
||||
|
||||
let entries = stmt.query_map([], |row| {
|
||||
let cat_str: String = row.get(1)?;
|
||||
let val_str: String = row.get(3)?;
|
||||
let sticky: i32 = row.get(4)?;
|
||||
let auto_load: i32 = row.get(5)?;
|
||||
|
||||
Ok(MemoryEntry {
|
||||
id: row.get(0)?,
|
||||
category: parse_context_category(&cat_str),
|
||||
key: row.get(2)?,
|
||||
value: serde_json::from_str(&val_str).unwrap_or(serde_json::Value::String(val_str)),
|
||||
sticky: sticky != 0,
|
||||
auto_load: auto_load != 0,
|
||||
last_used: row.get(6)?,
|
||||
use_count: row.get(7)?,
|
||||
})
|
||||
})?.collect::<SqlResult<Vec<_>>>()?;
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// Löscht einen Memory-Eintrag
|
||||
pub fn delete_memory_entry(&self, id: &str) -> SqlResult<()> {
|
||||
self.conn.execute("DELETE FROM memory WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============ Patterns ============
|
||||
|
||||
/// Speichert ein Pattern
|
||||
pub fn save_pattern(&self, pattern: &Pattern) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
"INSERT OR REPLACE INTO patterns (id, name, description, trigger_text, old_approach, new_approach, reason, occurrence_count, auto_corrected, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
|
||||
params![
|
||||
pattern.id,
|
||||
pattern.name,
|
||||
pattern.description,
|
||||
pattern.trigger,
|
||||
pattern.old_approach,
|
||||
pattern.new_approach,
|
||||
pattern.reason,
|
||||
pattern.occurrence_count,
|
||||
pattern.auto_corrected as i32,
|
||||
pattern.created_at,
|
||||
pattern.updated_at,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lädt alle Patterns
|
||||
pub fn load_patterns(&self) -> SqlResult<Vec<Pattern>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, name, description, trigger_text, old_approach, new_approach, reason, occurrence_count, auto_corrected, created_at, updated_at FROM patterns"
|
||||
)?;
|
||||
|
||||
let patterns = stmt.query_map([], |row| {
|
||||
let auto_corr: i32 = row.get(8)?;
|
||||
Ok(Pattern {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
description: row.get(2)?,
|
||||
trigger: row.get(3)?,
|
||||
old_approach: row.get(4)?,
|
||||
new_approach: row.get(5)?,
|
||||
reason: row.get(6)?,
|
||||
occurrence_count: row.get(7)?,
|
||||
auto_corrected: auto_corr != 0,
|
||||
created_at: row.get(9)?,
|
||||
updated_at: row.get(10)?,
|
||||
})
|
||||
})?.collect::<SqlResult<Vec<_>>>()?;
|
||||
|
||||
Ok(patterns)
|
||||
}
|
||||
|
||||
// ============ Sessions ============
|
||||
|
||||
/// Erstellt eine neue Session
|
||||
pub fn create_session(&self, session: &Session) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
"INSERT INTO sessions (id, claude_session_id, title, working_dir, message_count, token_input, token_output, cost_usd, status, created_at, updated_at, last_message)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
|
||||
params![
|
||||
session.id,
|
||||
session.claude_session_id,
|
||||
session.title,
|
||||
session.working_dir,
|
||||
session.message_count,
|
||||
session.token_input,
|
||||
session.token_output,
|
||||
session.cost_usd,
|
||||
session.status,
|
||||
session.created_at,
|
||||
session.updated_at,
|
||||
session.last_message,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Aktualisiert eine Session
|
||||
pub fn update_session(&self, session: &Session) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
"UPDATE sessions SET claude_session_id = ?2, title = ?3, message_count = ?4,
|
||||
token_input = ?5, token_output = ?6, cost_usd = ?7, status = ?8,
|
||||
updated_at = ?9, last_message = ?10
|
||||
WHERE id = ?1",
|
||||
params![
|
||||
session.id,
|
||||
session.claude_session_id,
|
||||
session.title,
|
||||
session.message_count,
|
||||
session.token_input,
|
||||
session.token_output,
|
||||
session.cost_usd,
|
||||
session.status,
|
||||
chrono::Local::now().to_rfc3339(),
|
||||
session.last_message,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lädt alle Sessions (neueste zuerst)
|
||||
pub fn load_sessions(&self, limit: usize) -> SqlResult<Vec<Session>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, claude_session_id, title, working_dir, message_count,
|
||||
token_input, token_output, cost_usd, status, created_at, updated_at, last_message
|
||||
FROM sessions ORDER BY updated_at DESC LIMIT ?1"
|
||||
)?;
|
||||
|
||||
let sessions = stmt.query_map(params![limit as i64], |row| {
|
||||
Ok(Session {
|
||||
id: row.get(0)?,
|
||||
claude_session_id: row.get(1)?,
|
||||
title: row.get(2)?,
|
||||
working_dir: row.get(3)?,
|
||||
message_count: row.get(4)?,
|
||||
token_input: row.get(5)?,
|
||||
token_output: row.get(6)?,
|
||||
cost_usd: row.get(7)?,
|
||||
status: row.get(8)?,
|
||||
created_at: row.get(9)?,
|
||||
updated_at: row.get(10)?,
|
||||
last_message: row.get(11)?,
|
||||
})
|
||||
})?.collect::<SqlResult<Vec<_>>>()?;
|
||||
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
/// Holt eine Session nach ID
|
||||
pub fn get_session(&self, id: &str) -> SqlResult<Option<Session>> {
|
||||
let result = self.conn.query_row(
|
||||
"SELECT id, claude_session_id, title, working_dir, message_count,
|
||||
token_input, token_output, cost_usd, status, created_at, updated_at, last_message
|
||||
FROM sessions WHERE id = ?1",
|
||||
params![id],
|
||||
|row| {
|
||||
Ok(Session {
|
||||
id: row.get(0)?,
|
||||
claude_session_id: row.get(1)?,
|
||||
title: row.get(2)?,
|
||||
working_dir: row.get(3)?,
|
||||
message_count: row.get(4)?,
|
||||
token_input: row.get(5)?,
|
||||
token_output: row.get(6)?,
|
||||
cost_usd: row.get(7)?,
|
||||
status: row.get(8)?,
|
||||
created_at: row.get(9)?,
|
||||
updated_at: row.get(10)?,
|
||||
last_message: row.get(11)?,
|
||||
})
|
||||
},
|
||||
);
|
||||
match result {
|
||||
Ok(s) => Ok(Some(s)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Löscht eine Session
|
||||
pub fn delete_session(&self, id: &str) -> SqlResult<()> {
|
||||
self.conn.execute("DELETE FROM sessions WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============ Settings ============
|
||||
|
||||
/// Speichert eine Einstellung
|
||||
pub fn set_setting(&self, key: &str, value: &str) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
"INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?1, ?2, ?3)",
|
||||
params![key, value, chrono::Local::now().to_rfc3339()],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Liest eine Einstellung
|
||||
pub fn get_setting(&self, key: &str) -> SqlResult<Option<String>> {
|
||||
let result = self.conn.query_row(
|
||||
"SELECT value FROM settings WHERE key = ?1",
|
||||
params![key],
|
||||
|row| row.get(0),
|
||||
);
|
||||
match result {
|
||||
Ok(val) => Ok(Some(val)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Statistiken ============
|
||||
|
||||
/// DB-Statistiken
|
||||
pub fn stats(&self) -> SqlResult<DbStats> {
|
||||
let permissions: usize = self.conn.query_row(
|
||||
"SELECT COUNT(*) FROM permissions", [], |row| row.get(0)
|
||||
)?;
|
||||
let audit_entries: usize = self.conn.query_row(
|
||||
"SELECT COUNT(*) FROM audit_log", [], |row| row.get(0)
|
||||
)?;
|
||||
let memory_entries: usize = self.conn.query_row(
|
||||
"SELECT COUNT(*) FROM memory", [], |row| row.get(0)
|
||||
)?;
|
||||
let patterns: usize = self.conn.query_row(
|
||||
"SELECT COUNT(*) FROM patterns", [], |row| row.get(0)
|
||||
)?;
|
||||
|
||||
// DB-Größe ermitteln
|
||||
let page_count: u64 = self.conn.query_row(
|
||||
"PRAGMA page_count", [], |row| row.get(0)
|
||||
)?;
|
||||
let page_size: u64 = self.conn.query_row(
|
||||
"PRAGMA page_size", [], |row| row.get(0)
|
||||
)?;
|
||||
let db_size_kb = (page_count * page_size) / 1024;
|
||||
|
||||
Ok(DbStats { permissions, audit_entries, memory_entries, patterns, db_size_kb })
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Hilfsfunktionen ============
|
||||
|
||||
fn parse_audit_category(s: &str) -> AuditCategory {
|
||||
match s {
|
||||
"guardrail" | "guard_rail" => AuditCategory::GuardRail,
|
||||
"pattern" => AuditCategory::Pattern,
|
||||
"hook" => AuditCategory::Hook,
|
||||
"skill" => AuditCategory::Skill,
|
||||
"setting" => AuditCategory::Setting,
|
||||
"mcp" => AuditCategory::MCP,
|
||||
"memory" => AuditCategory::Memory,
|
||||
_ => AuditCategory::Setting,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_audit_action(s: &str) -> AuditAction {
|
||||
match s {
|
||||
"create" => AuditAction::Create,
|
||||
"update" => AuditAction::Update,
|
||||
"delete" => AuditAction::Delete,
|
||||
"enable" => AuditAction::Enable,
|
||||
"disable" => AuditAction::Disable,
|
||||
_ => AuditAction::Update,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_context_category(s: &str) -> ContextCategory {
|
||||
match s {
|
||||
"Critical" => ContextCategory::Critical,
|
||||
"Pattern" => ContextCategory::Pattern,
|
||||
"Preference" => ContextCategory::Preference,
|
||||
"GuardRail" => ContextCategory::GuardRail,
|
||||
"Hook" => ContextCategory::Hook,
|
||||
"Skill" => ContextCategory::Skill,
|
||||
_ => ContextCategory::Pattern,
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Tauri Commands ============
|
||||
|
||||
pub type DbState = Arc<Mutex<Database>>;
|
||||
|
||||
/// DB initialisieren (falls Frontend es auslösen will)
|
||||
#[tauri::command]
|
||||
pub async fn init_database(app: AppHandle) -> Result<DbStats, String> {
|
||||
let state = app.state::<DbState>();
|
||||
let db = state.lock().unwrap();
|
||||
db.stats().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// DB-Statistiken abrufen
|
||||
#[tauri::command]
|
||||
pub async fn get_db_stats(app: AppHandle) -> Result<DbStats, String> {
|
||||
let state = app.state::<DbState>();
|
||||
let db = state.lock().unwrap();
|
||||
db.stats().map_err(|e| e.to_string())
|
||||
}
|
||||
|
|
@ -1,470 +0,0 @@
|
|||
// Claude Desktop — Guard-Rails System
|
||||
// Risiko-Klassifikation und Freigabe-Management
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
/// Risiko-Level einer Aktion
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RiskLevel {
|
||||
Safe, // Read, Glob, Grep → auto-approve
|
||||
Moderate, // Write, Edit, Git commit → Hinweis in Statusbar
|
||||
Critical, // Prod-Deploy, DB-Schema, Git push → Modal + Bestätigung
|
||||
Blocked, // rm -rf, force push main → hart blockiert
|
||||
}
|
||||
|
||||
/// Typ der Freigabe
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PermissionType {
|
||||
Session, // Gilt nur für aktuelle Session
|
||||
Permanent, // Dauerhaft gespeichert
|
||||
}
|
||||
|
||||
/// Eine Freigabe-Regel
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Permission {
|
||||
pub id: String,
|
||||
pub pattern: String, // z.B. "npm install *", "git commit -m *"
|
||||
pub tool: Option<String>, // z.B. "Bash", "Edit", None = alle
|
||||
pub path_pattern: Option<String>, // z.B. "/var/www/dolibarr/*"
|
||||
pub permission_type: PermissionType,
|
||||
pub action: PermissionAction,
|
||||
pub created_at: String,
|
||||
pub use_count: u32,
|
||||
pub last_used: Option<String>,
|
||||
}
|
||||
|
||||
/// Aktion einer Regel
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PermissionAction {
|
||||
Allow,
|
||||
Deny,
|
||||
}
|
||||
|
||||
/// Anfrage zur Freigabe
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PermissionRequest {
|
||||
pub id: String,
|
||||
pub tool: String,
|
||||
pub command: String,
|
||||
pub args: Option<serde_json::Value>,
|
||||
pub path: Option<String>,
|
||||
pub risk_level: RiskLevel,
|
||||
pub suggested_pattern: Option<String>,
|
||||
}
|
||||
|
||||
/// Antwort auf Freigabe-Anfrage
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PermissionResponse {
|
||||
pub request_id: String,
|
||||
pub allowed: bool,
|
||||
pub save_as: Option<PermissionType>,
|
||||
pub pattern: Option<String>,
|
||||
}
|
||||
|
||||
/// Guard-Rails Manager
|
||||
pub struct GuardRails {
|
||||
permissions: Vec<Permission>,
|
||||
session_permissions: Vec<Permission>,
|
||||
blocked_patterns: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for GuardRails {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl GuardRails {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
permissions: vec![],
|
||||
session_permissions: vec![],
|
||||
blocked_patterns: vec![
|
||||
// Immer blockiert
|
||||
"rm -rf /".to_string(),
|
||||
"rm -rf /*".to_string(),
|
||||
"rm -rf ~".to_string(),
|
||||
"git push --force origin main".to_string(),
|
||||
"git push --force origin master".to_string(),
|
||||
"git push -f origin main".to_string(),
|
||||
"DROP DATABASE".to_string(),
|
||||
"DROP TABLE".to_string(),
|
||||
"TRUNCATE TABLE".to_string(),
|
||||
"> /dev/sda".to_string(),
|
||||
"mkfs.".to_string(),
|
||||
"dd if=/dev/zero".to_string(),
|
||||
":(){:|:&};:".to_string(), // Fork bomb
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Klassifiziert das Risiko einer Aktion
|
||||
pub fn classify_risk(&self, tool: &str, command: &str, path: Option<&str>) -> RiskLevel {
|
||||
// Erst prüfen ob blockiert
|
||||
if self.is_blocked(command) {
|
||||
return RiskLevel::Blocked;
|
||||
}
|
||||
|
||||
// Tool-basierte Klassifikation
|
||||
match tool {
|
||||
// Sichere Tools
|
||||
"Read" | "Glob" | "Grep" | "WebFetch" | "WebSearch" => RiskLevel::Safe,
|
||||
|
||||
// Moderate Tools
|
||||
"Write" | "Edit" | "NotebookEdit" => {
|
||||
// Pfad-basierte Eskalation
|
||||
if let Some(p) = path {
|
||||
if self.is_production_path(p) {
|
||||
RiskLevel::Critical
|
||||
} else {
|
||||
RiskLevel::Moderate
|
||||
}
|
||||
} else {
|
||||
RiskLevel::Moderate
|
||||
}
|
||||
}
|
||||
|
||||
// Bash braucht Command-Analyse
|
||||
"Bash" => self.classify_bash_command(command, path),
|
||||
|
||||
// Task/Agent - abhängig vom Typ
|
||||
"Task" => RiskLevel::Moderate,
|
||||
|
||||
// Unbekannt = Moderate
|
||||
_ => RiskLevel::Moderate,
|
||||
}
|
||||
}
|
||||
|
||||
/// Klassifiziert Bash-Befehle
|
||||
fn classify_bash_command(&self, command: &str, path: Option<&str>) -> RiskLevel {
|
||||
let cmd_lower = command.to_lowercase();
|
||||
|
||||
// Sichere Befehle
|
||||
if cmd_lower.starts_with("ls ")
|
||||
|| cmd_lower.starts_with("pwd")
|
||||
|| cmd_lower.starts_with("echo ")
|
||||
|| cmd_lower.starts_with("cat ")
|
||||
|| cmd_lower.starts_with("head ")
|
||||
|| cmd_lower.starts_with("tail ")
|
||||
|| cmd_lower.starts_with("wc ")
|
||||
|| cmd_lower.starts_with("grep ")
|
||||
|| cmd_lower.starts_with("find ")
|
||||
|| cmd_lower.starts_with("which ")
|
||||
|| cmd_lower.starts_with("whoami")
|
||||
|| cmd_lower.starts_with("date")
|
||||
|| cmd_lower.starts_with("uname")
|
||||
{
|
||||
return RiskLevel::Safe;
|
||||
}
|
||||
|
||||
// Kritische Befehle
|
||||
if cmd_lower.contains("--force")
|
||||
|| cmd_lower.contains(" -f ")
|
||||
|| cmd_lower.starts_with("sudo ")
|
||||
|| cmd_lower.starts_with("su ")
|
||||
|| cmd_lower.contains("systemctl")
|
||||
|| cmd_lower.contains("service ")
|
||||
|| cmd_lower.contains("docker ")
|
||||
|| cmd_lower.contains("kubectl")
|
||||
|| cmd_lower.starts_with("rm ")
|
||||
|| cmd_lower.starts_with("mv ")
|
||||
|| cmd_lower.contains("chmod ")
|
||||
|| cmd_lower.contains("chown ")
|
||||
{
|
||||
// Prod-Pfad macht es noch kritischer
|
||||
if let Some(p) = path {
|
||||
if self.is_production_path(p) {
|
||||
return RiskLevel::Critical;
|
||||
}
|
||||
}
|
||||
|
||||
// rm ohne -r ist nur Moderate
|
||||
if cmd_lower.starts_with("rm ") && !cmd_lower.contains("-r") {
|
||||
return RiskLevel::Moderate;
|
||||
}
|
||||
|
||||
return RiskLevel::Critical;
|
||||
}
|
||||
|
||||
// Moderate Befehle
|
||||
if cmd_lower.starts_with("npm ")
|
||||
|| cmd_lower.starts_with("cargo ")
|
||||
|| cmd_lower.starts_with("git ")
|
||||
|| cmd_lower.starts_with("mkdir ")
|
||||
|| cmd_lower.starts_with("touch ")
|
||||
|| cmd_lower.starts_with("cp ")
|
||||
{
|
||||
// git push ist kritisch
|
||||
if cmd_lower.contains("git push") {
|
||||
return RiskLevel::Critical;
|
||||
}
|
||||
return RiskLevel::Moderate;
|
||||
}
|
||||
|
||||
// Default: Moderate
|
||||
RiskLevel::Moderate
|
||||
}
|
||||
|
||||
/// Prüft ob ein Befehl blockiert ist
|
||||
fn is_blocked(&self, command: &str) -> bool {
|
||||
let cmd_lower = command.to_lowercase();
|
||||
self.blocked_patterns
|
||||
.iter()
|
||||
.any(|p| cmd_lower.contains(&p.to_lowercase()))
|
||||
}
|
||||
|
||||
/// Prüft ob ein Pfad in Produktion liegt
|
||||
fn is_production_path(&self, path: &str) -> bool {
|
||||
let prod_patterns = [
|
||||
"/var/www/prod",
|
||||
"/var/www/production",
|
||||
"/opt/prod",
|
||||
"/home/prod",
|
||||
"/srv/prod",
|
||||
];
|
||||
prod_patterns.iter().any(|p| path.starts_with(p))
|
||||
}
|
||||
|
||||
/// Prüft ob eine Aktion erlaubt ist
|
||||
pub fn check_permission(&self, tool: &str, command: &str, path: Option<&str>) -> Option<&Permission> {
|
||||
// Erst Session-Permissions prüfen
|
||||
for perm in &self.session_permissions {
|
||||
if self.matches_permission(perm, tool, command, path) {
|
||||
return Some(perm);
|
||||
}
|
||||
}
|
||||
|
||||
// Dann permanente Permissions
|
||||
for perm in &self.permissions {
|
||||
if self.matches_permission(perm, tool, command, path) {
|
||||
return Some(perm);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Prüft ob eine Permission matcht
|
||||
fn matches_permission(&self, perm: &Permission, tool: &str, command: &str, path: Option<&str>) -> bool {
|
||||
// Tool prüfen
|
||||
if let Some(ref perm_tool) = perm.tool {
|
||||
if perm_tool != tool {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Pfad prüfen
|
||||
if let (Some(ref perm_path), Some(actual_path)) = (&perm.path_pattern, path) {
|
||||
if !self.matches_pattern(perm_path, actual_path) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Command/Pattern prüfen
|
||||
self.matches_pattern(&perm.pattern, command)
|
||||
}
|
||||
|
||||
/// Einfacher Pattern-Matcher mit * Wildcard
|
||||
fn matches_pattern(&self, pattern: &str, value: &str) -> bool {
|
||||
if pattern == "*" {
|
||||
return true;
|
||||
}
|
||||
|
||||
if pattern.contains('*') {
|
||||
let parts: Vec<&str> = pattern.split('*').collect();
|
||||
if parts.len() == 2 {
|
||||
let (prefix, suffix) = (parts[0], parts[1]);
|
||||
return value.starts_with(prefix) && value.ends_with(suffix);
|
||||
}
|
||||
}
|
||||
|
||||
pattern == value
|
||||
}
|
||||
|
||||
/// Fügt eine Permission hinzu
|
||||
pub fn add_permission(&mut self, permission: Permission) {
|
||||
match permission.permission_type {
|
||||
PermissionType::Session => self.session_permissions.push(permission),
|
||||
PermissionType::Permanent => self.permissions.push(permission),
|
||||
}
|
||||
}
|
||||
|
||||
/// Entfernt eine Permission
|
||||
pub fn remove_permission(&mut self, id: &str) {
|
||||
self.permissions.retain(|p| p.id != id);
|
||||
self.session_permissions.retain(|p| p.id != id);
|
||||
}
|
||||
|
||||
/// Session-Permissions löschen
|
||||
pub fn clear_session(&mut self) {
|
||||
self.session_permissions.clear();
|
||||
}
|
||||
|
||||
/// Alle Permissions abrufen
|
||||
pub fn get_all_permissions(&self) -> Vec<&Permission> {
|
||||
self.permissions.iter().chain(self.session_permissions.iter()).collect()
|
||||
}
|
||||
|
||||
/// Schlägt ein Pattern vor
|
||||
pub fn suggest_pattern(&self, _tool: &str, command: &str) -> String {
|
||||
// Für npm install: npm install *
|
||||
if command.starts_with("npm install ") {
|
||||
return "npm install *".to_string();
|
||||
}
|
||||
|
||||
// Für git commit: git commit -m *
|
||||
if command.starts_with("git commit ") {
|
||||
return "git commit *".to_string();
|
||||
}
|
||||
|
||||
// Für cargo: cargo *
|
||||
if command.starts_with("cargo ") {
|
||||
let parts: Vec<&str> = command.split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
return format!("cargo {} *", parts[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Default: exakter Befehl
|
||||
command.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Tauri Commands ============
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// Globaler Guard-Rails State
|
||||
pub type GuardState = Arc<Mutex<GuardRails>>;
|
||||
|
||||
/// Prüft eine Aktion und gibt Risiko-Level zurück
|
||||
#[tauri::command]
|
||||
pub async fn check_action(
|
||||
app: AppHandle,
|
||||
tool: String,
|
||||
command: String,
|
||||
path: Option<String>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let state = app.state::<GuardState>();
|
||||
let guard = state.lock().unwrap();
|
||||
|
||||
let risk = guard.classify_risk(&tool, &command, path.as_deref());
|
||||
|
||||
// Wenn blockiert, sofort ablehnen
|
||||
if risk == RiskLevel::Blocked {
|
||||
return Ok(serde_json::json!({
|
||||
"allowed": false,
|
||||
"risk": "blocked",
|
||||
"reason": "Diese Aktion ist aus Sicherheitsgründen blockiert."
|
||||
}));
|
||||
}
|
||||
|
||||
// Permission prüfen
|
||||
if let Some(perm) = guard.check_permission(&tool, &command, path.as_deref()) {
|
||||
return Ok(serde_json::json!({
|
||||
"allowed": perm.action == PermissionAction::Allow,
|
||||
"risk": format!("{:?}", risk).to_lowercase(),
|
||||
"matched_rule": perm.id
|
||||
}));
|
||||
}
|
||||
|
||||
// Kein Match - Frontend muss fragen
|
||||
let suggested = guard.suggest_pattern(&tool, &command);
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"allowed": risk == RiskLevel::Safe,
|
||||
"risk": format!("{:?}", risk).to_lowercase(),
|
||||
"needs_confirmation": risk != RiskLevel::Safe,
|
||||
"suggested_pattern": suggested
|
||||
}))
|
||||
}
|
||||
|
||||
/// Fügt eine Freigabe hinzu
|
||||
#[tauri::command]
|
||||
pub async fn add_permission(
|
||||
app: AppHandle,
|
||||
pattern: String,
|
||||
tool: Option<String>,
|
||||
path_pattern: Option<String>,
|
||||
permission_type: String,
|
||||
action: String,
|
||||
) -> Result<String, String> {
|
||||
let state = app.state::<GuardState>();
|
||||
let mut guard = state.lock().unwrap();
|
||||
|
||||
let perm_type = match permission_type.as_str() {
|
||||
"session" => PermissionType::Session,
|
||||
"permanent" => PermissionType::Permanent,
|
||||
_ => return Err("Ungültiger Permission-Typ".to_string()),
|
||||
};
|
||||
|
||||
let perm_action = match action.as_str() {
|
||||
"allow" => PermissionAction::Allow,
|
||||
"deny" => PermissionAction::Deny,
|
||||
_ => return Err("Ungültige Aktion".to_string()),
|
||||
};
|
||||
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
let permission = Permission {
|
||||
id: id.clone(),
|
||||
pattern,
|
||||
tool,
|
||||
path_pattern,
|
||||
permission_type: perm_type,
|
||||
action: perm_action,
|
||||
created_at: chrono::Local::now().to_rfc3339(),
|
||||
use_count: 0,
|
||||
last_used: None,
|
||||
};
|
||||
|
||||
guard.add_permission(permission.clone());
|
||||
|
||||
// Bei Permanent in SQLite speichern
|
||||
if perm_type == PermissionType::Permanent {
|
||||
let db_state = app.state::<Arc<Mutex<crate::db::Database>>>();
|
||||
let db_lock = db_state.lock().unwrap();
|
||||
db_lock.save_permission(&permission).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
println!("✅ Permission hinzugefügt: {}", id);
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Entfernt eine Freigabe
|
||||
#[tauri::command]
|
||||
pub async fn remove_permission(app: AppHandle, id: String) -> Result<(), String> {
|
||||
let state = app.state::<GuardState>();
|
||||
let mut guard = state.lock().unwrap();
|
||||
|
||||
guard.remove_permission(&id);
|
||||
|
||||
// Aus SQLite löschen
|
||||
let db_state = app.state::<Arc<Mutex<crate::db::Database>>>();
|
||||
let db_lock = db_state.lock().unwrap();
|
||||
let _ = db_lock.delete_permission(&id);
|
||||
|
||||
println!("🗑️ Permission entfernt: {}", id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Holt alle Freigaben
|
||||
#[tauri::command]
|
||||
pub async fn get_permissions(app: AppHandle) -> Result<Vec<Permission>, String> {
|
||||
let state = app.state::<GuardState>();
|
||||
let guard = state.lock().unwrap();
|
||||
|
||||
Ok(guard.get_all_permissions().into_iter().cloned().collect())
|
||||
}
|
||||
|
||||
/// Holt blockierte Patterns
|
||||
#[tauri::command]
|
||||
pub async fn get_blocked_patterns(app: AppHandle) -> Result<Vec<String>, String> {
|
||||
let state = app.state::<GuardState>();
|
||||
let guard = state.lock().unwrap();
|
||||
|
||||
Ok(guard.blocked_patterns.clone())
|
||||
}
|
||||
|
|
@ -6,10 +6,7 @@ use tauri::Manager;
|
|||
|
||||
mod audit;
|
||||
mod claude;
|
||||
mod db;
|
||||
mod guard;
|
||||
mod memory;
|
||||
mod session;
|
||||
|
||||
/// Initialisiert die App
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
|
|
@ -17,7 +14,6 @@ pub fn run() {
|
|||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.manage(Arc::new(Mutex::new(claude::ClaudeState::default())))
|
||||
.manage(guard::GuardState::new(Mutex::new(guard::GuardRails::new())))
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// Claude SDK
|
||||
claude::send_message,
|
||||
|
|
@ -32,60 +28,21 @@ pub fn run() {
|
|||
audit::get_audit_log,
|
||||
audit::add_audit_entry,
|
||||
audit::get_audit_stats,
|
||||
// Guard-Rails
|
||||
guard::check_action,
|
||||
guard::add_permission,
|
||||
guard::remove_permission,
|
||||
guard::get_permissions,
|
||||
guard::get_blocked_patterns,
|
||||
// Datenbank
|
||||
db::init_database,
|
||||
db::get_db_stats,
|
||||
// Sessions
|
||||
session::create_session,
|
||||
session::update_session,
|
||||
session::list_sessions,
|
||||
session::get_session,
|
||||
session::delete_session,
|
||||
session::resume_session,
|
||||
session::get_active_session,
|
||||
session::set_claude_session_id,
|
||||
])
|
||||
.setup(|app| {
|
||||
let handle = app.handle().clone();
|
||||
|
||||
println!("🤖 Claude Desktop gestartet");
|
||||
|
||||
// Datenbank initialisieren
|
||||
let app_dir = app.path().app_data_dir()
|
||||
.expect("Konnte App-Datenverzeichnis nicht ermitteln");
|
||||
std::fs::create_dir_all(&app_dir).ok();
|
||||
let db_path = app_dir.join("claude-desktop.db");
|
||||
println!("📦 Datenbank: {:?}", db_path);
|
||||
|
||||
let db = db::Database::open(&db_path)
|
||||
.expect("Datenbank konnte nicht geöffnet werden");
|
||||
app.manage(Arc::new(Mutex::new(db)));
|
||||
|
||||
// Guard-Rails: permanente Permissions aus DB laden
|
||||
{
|
||||
let db_state = handle.state::<Arc<Mutex<db::Database>>>();
|
||||
let guard_state = handle.state::<guard::GuardState>();
|
||||
let db_lock = db_state.lock().unwrap();
|
||||
let mut guard_lock = guard_state.lock().unwrap();
|
||||
if let Ok(perms) = db_lock.load_permissions() {
|
||||
for p in perms {
|
||||
guard_lock.add_permission(p);
|
||||
}
|
||||
println!("🛡️ {} permanente Permissions geladen", guard_lock.get_all_permissions().len());
|
||||
}
|
||||
}
|
||||
|
||||
// Gedächtnis-System laden
|
||||
// Gedächtnis-System beim Start laden
|
||||
tauri::async_runtime::spawn(async move {
|
||||
println!("🧠 Initialisiere Gedächtnis-System...");
|
||||
// TODO: memory::load_memory aufrufen
|
||||
});
|
||||
|
||||
// Bridge optional beim Start starten (oder lazy bei erster Nachricht)
|
||||
// let _ = claude::start_bridge(&app.handle());
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
use crate::db;
|
||||
use tauri::AppHandle;
|
||||
|
||||
/// Kategorien für Sticky Context (werden nie vergessen)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
|
|
@ -124,69 +121,44 @@ pub struct Pattern {
|
|||
pub async fn load_memory(app: AppHandle) -> Result<MemoryStats, String> {
|
||||
println!("🧠 Lade Gedächtnis-System...");
|
||||
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db_lock = state.lock().unwrap();
|
||||
let entries = db_lock.load_memory_entries().map_err(|e| e.to_string())?;
|
||||
|
||||
let mut by_category: HashMap<String, usize> = HashMap::new();
|
||||
let mut sticky_count = 0;
|
||||
for entry in &entries {
|
||||
let cat = format!("{:?}", entry.category);
|
||||
*by_category.entry(cat).or_insert(0) += 1;
|
||||
if entry.sticky {
|
||||
sticky_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
println!("🧠 {} Einträge geladen ({} sticky)", entries.len(), sticky_count);
|
||||
// TODO: Aus lokaler SQLite laden
|
||||
// TODO: Mit Remote claude-db synchronisieren
|
||||
|
||||
// Placeholder-Statistiken
|
||||
Ok(MemoryStats {
|
||||
total: entries.len(),
|
||||
sticky: sticky_count,
|
||||
by_category,
|
||||
total: 0,
|
||||
sticky: 0,
|
||||
by_category: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Holt den Sticky-Kontext für Claude
|
||||
#[tauri::command]
|
||||
pub async fn get_sticky_context(app: AppHandle) -> Result<Vec<MemoryEntry>, String> {
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db_lock = state.lock().unwrap();
|
||||
let entries = db_lock.load_memory_entries().map_err(|e| e.to_string())?;
|
||||
Ok(entries.into_iter().filter(|e| e.sticky).collect())
|
||||
pub async fn get_sticky_context() -> Result<Vec<MemoryEntry>, String> {
|
||||
// TODO: Echte Implementierung
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
/// Speichert eine neue Vorgehensweise
|
||||
#[tauri::command]
|
||||
pub async fn save_pattern(app: AppHandle, pattern: Pattern) -> Result<(), String> {
|
||||
pub async fn save_pattern(pattern: Pattern) -> Result<(), String> {
|
||||
println!("📝 Speichere Vorgehensweise: {}", pattern.name);
|
||||
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db_lock = state.lock().unwrap();
|
||||
db_lock.save_pattern(&pattern).map_err(|e| e.to_string())
|
||||
// TODO: In SQLite speichern
|
||||
// TODO: Mit claude-db synchronisieren
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Erkennt ein Problem und schlägt Korrektur vor
|
||||
#[tauri::command]
|
||||
pub async fn detect_issue(
|
||||
app: AppHandle,
|
||||
error_message: String,
|
||||
_context: String,
|
||||
context: String,
|
||||
) -> Result<Option<Pattern>, String> {
|
||||
let preview_len = error_message.len().min(50);
|
||||
println!("🔍 Prüfe auf bekannte Probleme: {}", &error_message[..preview_len]);
|
||||
println!("🔍 Prüfe auf bekannte Probleme: {}", &error_message[..error_message.len().min(50)]);
|
||||
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db_lock = state.lock().unwrap();
|
||||
let patterns = db_lock.load_patterns().map_err(|e| e.to_string())?;
|
||||
|
||||
// Einfaches Pattern-Matching: Trigger im Fehlertext suchen
|
||||
let error_lower = error_message.to_lowercase();
|
||||
for pattern in patterns {
|
||||
if error_lower.contains(&pattern.trigger.to_lowercase()) {
|
||||
return Ok(Some(pattern));
|
||||
}
|
||||
}
|
||||
// TODO: Pattern-Matching gegen bekannte Probleme
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,155 +0,0 @@
|
|||
// Claude Desktop — Session-Verwaltung
|
||||
// Sessions bleiben permanent gespeichert bis der User sie löscht
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
use crate::db::{self, Session};
|
||||
|
||||
// ============ Tauri Commands ============
|
||||
|
||||
/// Neue Session erstellen
|
||||
#[tauri::command]
|
||||
pub async fn create_session(
|
||||
app: AppHandle,
|
||||
title: String,
|
||||
working_dir: Option<String>,
|
||||
) -> Result<Session, String> {
|
||||
let session = Session {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
claude_session_id: None,
|
||||
title,
|
||||
working_dir,
|
||||
message_count: 0,
|
||||
token_input: 0,
|
||||
token_output: 0,
|
||||
cost_usd: 0.0,
|
||||
status: "active".to_string(),
|
||||
created_at: chrono::Local::now().to_rfc3339(),
|
||||
updated_at: chrono::Local::now().to_rfc3339(),
|
||||
last_message: None,
|
||||
};
|
||||
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db = state.lock().unwrap();
|
||||
db.create_session(&session).map_err(|e| e.to_string())?;
|
||||
|
||||
// Als aktive Session speichern
|
||||
db.set_setting("active_session_id", &session.id).map_err(|e| e.to_string())?;
|
||||
|
||||
println!("📝 Neue Session: {} ({})", session.title, session.id);
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
/// Session aktualisieren (nach Nachrichten, Token-Update, etc.)
|
||||
#[tauri::command]
|
||||
pub async fn update_session(
|
||||
app: AppHandle,
|
||||
session: Session,
|
||||
) -> Result<(), String> {
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db = state.lock().unwrap();
|
||||
db.update_session(&session).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Alle Sessions laden
|
||||
#[tauri::command]
|
||||
pub async fn list_sessions(
|
||||
app: AppHandle,
|
||||
limit: Option<usize>,
|
||||
) -> Result<Vec<Session>, String> {
|
||||
let limit = limit.unwrap_or(50);
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db = state.lock().unwrap();
|
||||
db.load_sessions(limit).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Session nach ID laden
|
||||
#[tauri::command]
|
||||
pub async fn get_session(
|
||||
app: AppHandle,
|
||||
id: String,
|
||||
) -> Result<Option<Session>, String> {
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db = state.lock().unwrap();
|
||||
db.get_session(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Session löschen
|
||||
#[tauri::command]
|
||||
pub async fn delete_session(
|
||||
app: AppHandle,
|
||||
id: String,
|
||||
) -> Result<(), String> {
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db = state.lock().unwrap();
|
||||
db.delete_session(&id).map_err(|e| e.to_string())?;
|
||||
|
||||
// Falls es die aktive Session war, Setting löschen
|
||||
if let Ok(Some(active_id)) = db.get_setting("active_session_id") {
|
||||
if active_id == id {
|
||||
let _ = db.set_setting("active_session_id", "");
|
||||
}
|
||||
}
|
||||
|
||||
println!("🗑️ Session gelöscht: {}", id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Session fortsetzen — setzt die aktive Session und gibt die claude_session_id zurück
|
||||
#[tauri::command]
|
||||
pub async fn resume_session(
|
||||
app: AppHandle,
|
||||
id: String,
|
||||
) -> Result<Session, String> {
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db = state.lock().unwrap();
|
||||
|
||||
let session = db.get_session(&id)
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| format!("Session {} nicht gefunden", id))?;
|
||||
|
||||
// Als aktive Session setzen
|
||||
db.set_setting("active_session_id", &session.id).map_err(|e| e.to_string())?;
|
||||
|
||||
println!("▶️ Session fortgesetzt: {} (claude: {:?})", session.title, session.claude_session_id);
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
/// Aktive Session holen (nach App-Start)
|
||||
#[tauri::command]
|
||||
pub async fn get_active_session(
|
||||
app: AppHandle,
|
||||
) -> Result<Option<Session>, String> {
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db = state.lock().unwrap();
|
||||
|
||||
if let Ok(Some(id)) = db.get_setting("active_session_id") {
|
||||
if !id.is_empty() {
|
||||
return db.get_session(&id).map_err(|e| e.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Claude Session-ID speichern (kommt von der Bridge nach erstem Request)
|
||||
#[tauri::command]
|
||||
pub async fn set_claude_session_id(
|
||||
app: AppHandle,
|
||||
session_id: String,
|
||||
claude_session_id: String,
|
||||
) -> Result<(), String> {
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db = state.lock().unwrap();
|
||||
|
||||
if let Ok(Some(mut session)) = db.get_session(&session_id) {
|
||||
session.claude_session_id = Some(claude_session_id.clone());
|
||||
db.update_session(&session).map_err(|e| e.to_string())?;
|
||||
println!("🔗 Claude Session-ID gesetzt: {} → {}", session_id, claude_session_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -34,7 +34,19 @@
|
|||
},
|
||||
"plugins": {
|
||||
"shell": {
|
||||
"open": true
|
||||
"open": true,
|
||||
"scope": [
|
||||
{
|
||||
"name": "node",
|
||||
"cmd": "node",
|
||||
"args": true
|
||||
},
|
||||
{
|
||||
"name": "npm",
|
||||
"cmd": "npm",
|
||||
"args": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
106
src/app.css
106
src/app.css
|
|
@ -1,29 +1,17 @@
|
|||
/* Claude Desktop — Basis-Styles (AWL Dark Theme) */
|
||||
/* Claude Desktop — Basis-Styles */
|
||||
|
||||
:root {
|
||||
/* AWL Dark Farbschema — basierend auf KDE Breeze Dark */
|
||||
--bg-primary: #202326; /* Body/Window BG */
|
||||
--bg-secondary: #272c31; /* Top-Menü / Header BG */
|
||||
--bg-tertiary: #292c30; /* Button/Tabellen-Header BG */
|
||||
--bg-input: #141618; /* Input-Felder / ungerade Zeilen */
|
||||
--bg-hover: #2a2e33; /* Hover-State */
|
||||
--bg-selected: #1e5774; /* Ausgewählte Elemente */
|
||||
|
||||
--text-primary: #cccccc; /* Haupttext (VSCode Dark+ Niveau) */
|
||||
--text-secondary: #a1a9b1; /* Sekundärtext (ForegroundInactive) */
|
||||
--text-heading: #d4d4d4; /* Titel/Überschriften */
|
||||
|
||||
--accent: #3daee9; /* DecorationFocus (Breeze Blau) */
|
||||
--accent-hover: #4dbdf9; /* Heller beim Hover */
|
||||
--link: #1d99f3; /* ForegroundLink */
|
||||
|
||||
--success: #27ae60; /* Grün (KDE Breeze) */
|
||||
--warning: #f67400; /* Orange (KDE Breeze) */
|
||||
--error: #da4453; /* Rot (KDE Breeze) */
|
||||
|
||||
/* Rahmen */
|
||||
--border: #3b3f44; /* Dezente Trennlinien */
|
||||
--border-active: #3daee9; /* Aktive Elemente */
|
||||
/* Farbschema */
|
||||
--bg-primary: #1a1a2e;
|
||||
--bg-secondary: #16213e;
|
||||
--bg-tertiary: #0f3460;
|
||||
--text-primary: #eaeaea;
|
||||
--text-secondary: #a0a0a0;
|
||||
--accent: #e94560;
|
||||
--accent-hover: #ff6b6b;
|
||||
--success: #4ade80;
|
||||
--warning: #fbbf24;
|
||||
--error: #ef4444;
|
||||
|
||||
/* Abstände */
|
||||
--spacing-xs: 0.25rem;
|
||||
|
|
@ -33,18 +21,29 @@
|
|||
--spacing-xl: 2rem;
|
||||
|
||||
/* Border-Radius */
|
||||
--radius-sm: 3px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 10px;
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
|
||||
/* Schatten */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.5);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
|
||||
|
||||
/* Font */
|
||||
--font-mono: 'Hack', 'JetBrains Mono', 'Fira Code', monospace;
|
||||
--font-sans: 'Noto Sans', 'Inter', system-ui, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
--font-sans: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* Light Mode */
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--bg-primary: #f8f9fa;
|
||||
--bg-secondary: #e9ecef;
|
||||
--bg-tertiary: #dee2e6;
|
||||
--text-primary: #212529;
|
||||
--text-secondary: #6c757d;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
|
|
@ -59,21 +58,20 @@ html, body {
|
|||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Scrollbar — KDE-Style, dezent */
|
||||
/* Scrollbar-Styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-primary);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
|
|
@ -91,52 +89,34 @@ button {
|
|||
}
|
||||
|
||||
/* Input-Reset */
|
||||
input, textarea, select {
|
||||
input, textarea {
|
||||
font-family: inherit;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-input);
|
||||
border: none;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius-md);
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
input:focus, textarea:focus, select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
input:focus, textarea:focus {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Code-Blöcke */
|
||||
code, pre {
|
||||
font-family: var(--font-mono);
|
||||
background: var(--bg-input);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0.1em 0.3em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: var(--spacing-md);
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Überschriften */
|
||||
h1, h2, h3, h4 {
|
||||
color: var(--text-heading);
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a {
|
||||
color: var(--link);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Animationen */
|
||||
|
|
@ -157,9 +137,3 @@ a:hover {
|
|||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Selection */
|
||||
::selection {
|
||||
background: var(--bg-selected);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
interface AuditEntry {
|
||||
id: string;
|
||||
|
|
@ -21,7 +20,6 @@
|
|||
// Kategorie-Icons
|
||||
const categoryIcons: Record<string, string> = {
|
||||
guard_rail: '🛡️',
|
||||
guardrail: '🛡️',
|
||||
pattern: '📋',
|
||||
hook: '🪝',
|
||||
skill: '🎯',
|
||||
|
|
@ -39,17 +37,45 @@
|
|||
disable: '❌'
|
||||
};
|
||||
|
||||
async function loadEntries() {
|
||||
try {
|
||||
entries = await invoke('get_audit_log', { limit: 50 });
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden des Audit-Logs:', err);
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
onMount(async () => {
|
||||
// TODO: Echte Daten laden via Tauri
|
||||
// const result = await invoke('get_audit_log', { limit: 50 });
|
||||
|
||||
onMount(() => {
|
||||
loadEntries();
|
||||
// Placeholder-Daten
|
||||
entries = [
|
||||
{
|
||||
id: '1',
|
||||
timestamp: new Date().toISOString(),
|
||||
category: 'guard_rail',
|
||||
action: 'update',
|
||||
item_name: 'npm install *',
|
||||
old_value: 'deny',
|
||||
new_value: 'allow_permanent',
|
||||
reason: 'Häufig verwendet, sicher',
|
||||
auto_corrected: false
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timestamp: new Date(Date.now() - 3600000).toISOString(),
|
||||
category: 'pattern',
|
||||
action: 'create',
|
||||
item_name: 'Tauri in nix-shell starten',
|
||||
old_value: 'cargo tauri dev',
|
||||
new_value: 'nix-shell --run "cargo tauri dev"',
|
||||
reason: 'libbz2 fehlte ohne nix-shell',
|
||||
auto_corrected: true
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timestamp: new Date(Date.now() - 7200000).toISOString(),
|
||||
category: 'hook',
|
||||
action: 'enable',
|
||||
item_name: 'post-git-commit.sh',
|
||||
reason: 'Changelog-Erinnerung aktiviert',
|
||||
auto_corrected: false
|
||||
}
|
||||
];
|
||||
loading = false;
|
||||
});
|
||||
|
||||
function formatTime(timestamp: string): string {
|
||||
|
|
|
|||
|
|
@ -1,47 +1,20 @@
|
|||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { messages, currentInput, isProcessing, addMessage } from '$lib/stores/app';
|
||||
import { marked } from 'marked';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
// marked konfigurieren: kein sanitize nötig (lokale App, kein User-Input von extern)
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
});
|
||||
|
||||
// Markdown zu HTML konvertieren
|
||||
function renderMarkdown(text: string): string {
|
||||
try {
|
||||
return marked.parse(text) as string;
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-Scroll zum Ende
|
||||
let messagesContainer: HTMLDivElement;
|
||||
|
||||
async function scrollToBottom() {
|
||||
await tick();
|
||||
if (messagesContainer) {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Bei neuen Nachrichten scrollen
|
||||
$: if ($messages.length) scrollToBottom();
|
||||
|
||||
async function sendMessage() {
|
||||
const text = $currentInput.trim();
|
||||
if (!text || $isProcessing) return;
|
||||
|
||||
// Nachricht hinzufügen
|
||||
addMessage('user', text);
|
||||
$currentInput = '';
|
||||
$isProcessing = true;
|
||||
|
||||
try {
|
||||
// An Claude senden via Tauri
|
||||
await invoke('send_message', { message: text });
|
||||
// Antwort kommt über Events (claude-text, agent-stopped)
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Senden:', err);
|
||||
addMessage('system', `Fehler: ${err}`);
|
||||
|
|
@ -60,15 +33,13 @@
|
|||
<div class="chat-panel">
|
||||
<div class="chat-header">
|
||||
<h2>💬 Chat</h2>
|
||||
<span class="msg-count">{$messages.length} Nachrichten</span>
|
||||
</div>
|
||||
|
||||
<div class="chat-messages" bind:this={messagesContainer}>
|
||||
<div class="chat-messages">
|
||||
{#if $messages.length === 0}
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">🤖</div>
|
||||
<p>Starte eine Konversation mit Claude.</p>
|
||||
<p class="hint">Enter = Senden, Shift+Enter = Neue Zeile, Escape = Stopp</p>
|
||||
<p class="hint">Drücke Enter zum Senden, Shift+Enter für neue Zeile.</p>
|
||||
</div>
|
||||
{:else}
|
||||
{#each $messages as message}
|
||||
|
|
@ -88,18 +59,14 @@
|
|||
</span>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
{#if message.role === 'assistant'}
|
||||
{@html renderMarkdown(message.content)}
|
||||
{:else}
|
||||
{message.content}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if $isProcessing}
|
||||
<div class="message assistant typing-msg">
|
||||
<div class="message assistant">
|
||||
<div class="message-header">
|
||||
<span class="message-role">🤖 Claude</span>
|
||||
</div>
|
||||
|
|
@ -125,11 +92,7 @@
|
|||
on:click={sendMessage}
|
||||
disabled={!$currentInput.trim() || $isProcessing}
|
||||
>
|
||||
{#if $isProcessing}
|
||||
⏳
|
||||
{:else}
|
||||
➤
|
||||
{/if}
|
||||
Senden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -142,9 +105,6 @@
|
|||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
|
|
@ -155,11 +115,6 @@
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
.msg-count {
|
||||
font-size: 0.625rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
|
@ -179,39 +134,29 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: var(--spacing-md);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state .hint {
|
||||
font-size: 0.75rem;
|
||||
margin-top: var(--spacing-sm);
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-secondary);
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
background: var(--bg-tertiary);
|
||||
margin-left: auto;
|
||||
margin-left: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.message.assistant {
|
||||
margin-right: auto;
|
||||
margin-right: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.message.system {
|
||||
background: rgba(233, 69, 96, 0.1);
|
||||
border-left: 3px solid var(--accent);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
|
|
@ -222,105 +167,19 @@
|
|||
}
|
||||
|
||||
.message-role {
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 0.6rem;
|
||||
font-size: 0.625rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.message-content {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Markdown-Styles innerhalb von Nachrichten */
|
||||
.message-content :global(p) {
|
||||
margin: 0.3em 0;
|
||||
}
|
||||
|
||||
.message-content :global(p:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.message-content :global(p:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.message-content :global(code) {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8rem;
|
||||
padding: 0.1em 0.35em;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.message-content :global(pre) {
|
||||
margin: 0.5em 0;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--radius-md);
|
||||
overflow-x: auto;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.message-content :global(pre code) {
|
||||
padding: 0;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.message-content :global(ul), .message-content :global(ol) {
|
||||
margin: 0.3em 0;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.message-content :global(li) {
|
||||
margin: 0.15em 0;
|
||||
}
|
||||
|
||||
.message-content :global(strong) {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.message-content :global(a) {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.message-content :global(a:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.message-content :global(blockquote) {
|
||||
border-left: 3px solid var(--bg-tertiary);
|
||||
padding-left: var(--spacing-sm);
|
||||
margin: 0.3em 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.message-content :global(h1), .message-content :global(h2), .message-content :global(h3) {
|
||||
margin: 0.5em 0 0.2em;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.message-content :global(table) {
|
||||
border-collapse: collapse;
|
||||
margin: 0.3em 0;
|
||||
font-size: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message-content :global(th), .message-content :global(td) {
|
||||
border: 1px solid var(--bg-tertiary);
|
||||
padding: 0.3em 0.5em;
|
||||
}
|
||||
|
||||
.message-content :global(th) {
|
||||
background: var(--bg-secondary);
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Typing-Animation */
|
||||
|
|
@ -349,7 +208,7 @@
|
|||
.chat-input {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid var(--bg-tertiary);
|
||||
}
|
||||
|
|
@ -357,34 +216,25 @@
|
|||
.chat-input textarea {
|
||||
flex: 1;
|
||||
resize: none;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
border-radius: var(--radius-md);
|
||||
transition: all 0.2s ease;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.send-button:hover:not(:disabled) {
|
||||
background: var(--accent-hover);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.send-button:disabled {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,367 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
interface Permission {
|
||||
id: string;
|
||||
pattern: string;
|
||||
tool: string | null;
|
||||
path_pattern: string | null;
|
||||
permission_type: string;
|
||||
action: string;
|
||||
created_at: string;
|
||||
use_count: number;
|
||||
last_used: string | null;
|
||||
}
|
||||
|
||||
let permissions: Permission[] = [];
|
||||
let blockedPatterns: string[] = [];
|
||||
let loading = true;
|
||||
let showAddForm = false;
|
||||
|
||||
// Formular-State
|
||||
let newPattern = '';
|
||||
let newTool = '';
|
||||
let newAction: 'allow' | 'deny' = 'allow';
|
||||
let newType: 'session' | 'permanent' = 'permanent';
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
permissions = await invoke('get_permissions');
|
||||
blockedPatterns = await invoke('get_blocked_patterns');
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden:', err);
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
async function addPermission() {
|
||||
if (!newPattern.trim()) return;
|
||||
try {
|
||||
await invoke('add_permission', {
|
||||
pattern: newPattern,
|
||||
tool: newTool || null,
|
||||
pathPattern: null,
|
||||
permissionType: newType,
|
||||
action: newAction,
|
||||
});
|
||||
newPattern = '';
|
||||
newTool = '';
|
||||
showAddForm = false;
|
||||
await loadData();
|
||||
} catch (err) {
|
||||
console.error('Fehler:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function removePermission(id: string) {
|
||||
try {
|
||||
await invoke('remove_permission', { id });
|
||||
await loadData();
|
||||
} catch (err) {
|
||||
console.error('Fehler:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAction() {
|
||||
if (!newPattern.trim()) return;
|
||||
try {
|
||||
const result = await invoke('check_action', {
|
||||
tool: newTool || 'Bash',
|
||||
command: newPattern,
|
||||
path: null,
|
||||
});
|
||||
console.log('Check-Ergebnis:', result);
|
||||
} catch (err) {
|
||||
console.error('Fehler:', err);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="guard-panel">
|
||||
<div class="panel-header">
|
||||
<h2>🛡️ Guard-Rails</h2>
|
||||
<button class="btn-add" on:click={() => showAddForm = !showAddForm}>
|
||||
{showAddForm ? '✕' : '+ Regel'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Neue Regel hinzufügen -->
|
||||
{#if showAddForm}
|
||||
<div class="add-form">
|
||||
<div class="form-row">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newPattern}
|
||||
placeholder="Pattern (z.B. npm install *)"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newTool}
|
||||
placeholder="Tool (optional, z.B. Bash)"
|
||||
/>
|
||||
<select bind:value={newAction}>
|
||||
<option value="allow">Erlauben</option>
|
||||
<option value="deny">Verweigern</option>
|
||||
</select>
|
||||
<select bind:value={newType}>
|
||||
<option value="permanent">Dauerhaft</option>
|
||||
<option value="session">Session</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn-save" on:click={addPermission}>Speichern</button>
|
||||
<button class="btn-test" on:click={checkAction}>Testen</button>
|
||||
</div>
|
||||
</div>
|
||||
{/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}
|
||||
<div class="empty-hint">Keine Regeln definiert</div>
|
||||
{:else}
|
||||
<div class="rules-list">
|
||||
{#each permissions as perm}
|
||||
<div class="rule-item" class:deny={perm.action === 'Deny'}>
|
||||
<div class="rule-main">
|
||||
<span class="rule-action" class:allow={perm.action === 'Allow'} class:deny={perm.action === 'Deny'}>
|
||||
{perm.action === 'Allow' ? '✅' : '🚫'}
|
||||
</span>
|
||||
<code class="rule-pattern">{perm.pattern}</code>
|
||||
{#if perm.tool}
|
||||
<span class="rule-tool">{perm.tool}</span>
|
||||
{/if}
|
||||
</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>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Blockierte Patterns -->
|
||||
<div class="section">
|
||||
<h3>🚫 Immer blockiert ({blockedPatterns.length})</h3>
|
||||
<div class="blocked-list">
|
||||
{#each blockedPatterns as pattern}
|
||||
<div class="blocked-item">
|
||||
<code>{pattern}</code>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.guard-panel {
|
||||
padding: var(--spacing-md);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.panel-header h2 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.form-row input {
|
||||
flex: 1;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.form-row select {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.form-actions button {
|
||||
flex: 1;
|
||||
padding: var(--spacing-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
background: var(--success);
|
||||
color: var(--bg-primary);
|
||||
}
|
||||
|
||||
.btn-test {
|
||||
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 {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
/* Regeln */
|
||||
.rules-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.rule-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
border-left: 3px solid var(--success);
|
||||
}
|
||||
|
||||
.rule-item.deny {
|
||||
border-left-color: var(--error);
|
||||
}
|
||||
|
||||
.rule-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rule-pattern {
|
||||
font-size: 0.75rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rule-tool {
|
||||
font-size: 0.625rem;
|
||||
padding: 1px 4px;
|
||||
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);
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
font-size: 0.75rem;
|
||||
padding: 2px;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Blockiert */
|
||||
.blocked-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.blocked-item {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.blocked-item code {
|
||||
color: var(--error);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
interface MemoryStats {
|
||||
total: number;
|
||||
|
|
@ -28,23 +27,33 @@
|
|||
MCP: '🔌'
|
||||
};
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
stats = await invoke('load_memory');
|
||||
lastSync = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden:', err);
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
onMount(async () => {
|
||||
// TODO: Echte Daten laden via Tauri
|
||||
// const result = await invoke('load_memory');
|
||||
|
||||
onMount(() => {
|
||||
loadStats();
|
||||
// Placeholder-Daten
|
||||
stats = {
|
||||
total: 95,
|
||||
sticky: 23,
|
||||
by_category: {
|
||||
Critical: 8,
|
||||
Pattern: 47,
|
||||
Preference: 12,
|
||||
GuardRail: 15,
|
||||
Hook: 5,
|
||||
Skill: 8
|
||||
}
|
||||
};
|
||||
lastSync = 'vor 2 Minuten';
|
||||
loading = false;
|
||||
});
|
||||
|
||||
async function syncNow() {
|
||||
loading = true;
|
||||
await loadStats();
|
||||
// TODO: Mit claude-db synchronisieren
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
lastSync = 'gerade eben';
|
||||
loading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,344 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { messages, clearAll, isProcessing } from '$lib/stores/app';
|
||||
|
||||
interface Session {
|
||||
id: string;
|
||||
claude_session_id: string | null;
|
||||
title: string;
|
||||
working_dir: string | null;
|
||||
message_count: number;
|
||||
token_input: number;
|
||||
token_output: number;
|
||||
cost_usd: number;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
last_message: string | null;
|
||||
}
|
||||
|
||||
let sessions: Session[] = [];
|
||||
let activeSessionId: string | null = null;
|
||||
let loading = true;
|
||||
let showNewForm = false;
|
||||
let newTitle = '';
|
||||
|
||||
async function loadSessions() {
|
||||
try {
|
||||
sessions = await invoke('list_sessions', { limit: 50 });
|
||||
const active: Session | null = await invoke('get_active_session');
|
||||
activeSessionId = active?.id || null;
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden der Sessions:', err);
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadSessions();
|
||||
});
|
||||
|
||||
async function createSession() {
|
||||
const title = newTitle.trim() || `Session ${new Date().toLocaleDateString('de-DE')}`;
|
||||
try {
|
||||
const session: Session = await invoke('create_session', {
|
||||
title,
|
||||
workingDir: null,
|
||||
});
|
||||
activeSessionId = session.id;
|
||||
clearAll();
|
||||
newTitle = '';
|
||||
showNewForm = false;
|
||||
await loadSessions();
|
||||
} catch (err) {
|
||||
console.error('Fehler:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function resumeSession(id: string) {
|
||||
if (id === activeSessionId) return;
|
||||
try {
|
||||
const session: Session = await invoke('resume_session', { id });
|
||||
activeSessionId = session.id;
|
||||
clearAll();
|
||||
// TODO: Nachrichten aus Session-Historie laden
|
||||
} catch (err) {
|
||||
console.error('Fehler:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSession(id: string) {
|
||||
try {
|
||||
await invoke('delete_session', { id });
|
||||
if (activeSessionId === id) {
|
||||
activeSessionId = null;
|
||||
clearAll();
|
||||
}
|
||||
await loadSessions();
|
||||
} catch (err) {
|
||||
console.error('Fehler:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
|
||||
if (diff < 60000) return 'gerade eben';
|
||||
if (diff < 3600000) return `vor ${Math.floor(diff / 60000)} Min`;
|
||||
if (diff < 86400000) return `vor ${Math.floor(diff / 3600000)} Std`;
|
||||
if (diff < 172800000) return 'gestern';
|
||||
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
|
||||
}
|
||||
|
||||
function formatCost(usd: number): string {
|
||||
if (usd === 0) return '';
|
||||
return `$${usd.toFixed(3)}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="session-list">
|
||||
<div class="session-header">
|
||||
<h2>💬 Sessions</h2>
|
||||
<button class="btn-new" on:click={() => showNewForm = !showNewForm}>
|
||||
{showNewForm ? '✕' : '+ Neu'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if showNewForm}
|
||||
<div class="new-form">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newTitle}
|
||||
placeholder="Session-Titel (optional)"
|
||||
on:keydown={(e) => e.key === 'Enter' && createSession()}
|
||||
/>
|
||||
<button class="btn-create" on:click={createSession}>Erstellen</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<div class="loading">Lade Sessions...</div>
|
||||
{:else if sessions.length === 0}
|
||||
<div class="empty">
|
||||
<p>Keine Sessions vorhanden.</p>
|
||||
<button class="btn-first" on:click={() => { showNewForm = true; }}>
|
||||
Erste Session starten
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="sessions">
|
||||
{#each sessions as session}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="session-item"
|
||||
class:active={session.id === activeSessionId}
|
||||
on:click={() => resumeSession(session.id)}
|
||||
>
|
||||
<div class="session-main">
|
||||
<span class="session-status">
|
||||
{#if session.id === activeSessionId}
|
||||
🟢
|
||||
{:else if session.claude_session_id}
|
||||
⏸️
|
||||
{:else}
|
||||
⚪
|
||||
{/if}
|
||||
</span>
|
||||
<div class="session-info">
|
||||
<span class="session-title">{session.title}</span>
|
||||
<span class="session-meta">
|
||||
{session.message_count} Nachrichten
|
||||
{#if session.cost_usd > 0}
|
||||
· {formatCost(session.cost_usd)}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-right">
|
||||
<span class="session-time">{formatDate(session.updated_at)}</span>
|
||||
<button
|
||||
class="btn-delete"
|
||||
on:click|stopPropagation={() => deleteSession(session.id)}
|
||||
title="Session löschen"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.session-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--border);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.session-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.session-header h2 {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-new {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-new:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* Neues Session Formular */
|
||||
.new-form {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-sm);
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.new-form input {
|
||||
flex: 1;
|
||||
font-size: 0.75rem;
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
}
|
||||
|
||||
.btn-create {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
background: var(--success);
|
||||
color: white;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.loading, .empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
padding: var(--spacing-md);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-first {
|
||||
margin-top: var(--spacing-sm);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Session-Items */
|
||||
.sessions {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.session-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
transition: background 0.15s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.session-item:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.session-item.active {
|
||||
background: var(--bg-tertiary);
|
||||
border-left: 3px solid var(--accent);
|
||||
}
|
||||
|
||||
.session-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.session-status {
|
||||
font-size: 0.6rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.session-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.session-title {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.session-meta {
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.session-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.session-time {
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
font-size: 0.65rem;
|
||||
opacity: 0;
|
||||
padding: 2px;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.session-item:hover .btn-delete {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -32,28 +32,28 @@
|
|||
justify-content: center;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
background: linear-gradient(135deg, var(--error), #c0392b);
|
||||
background: linear-gradient(135deg, #dc2626, #b91c1c);
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border-radius: var(--radius-md);
|
||||
border: 2px solid var(--error);
|
||||
border: 2px solid #ef4444;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 4px 15px rgba(218, 68, 83, 0.3);
|
||||
box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
|
||||
}
|
||||
|
||||
.stop-button:not(.disabled):hover {
|
||||
background: linear-gradient(135deg, #e74c3c, var(--error));
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 20px rgba(218, 68, 83, 0.4);
|
||||
box-shadow: 0 6px 20px rgba(220, 38, 38, 0.5);
|
||||
}
|
||||
|
||||
.stop-button:not(.disabled):active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 10px rgba(218, 68, 83, 0.3);
|
||||
box-shadow: 0 2px 10px rgba(220, 38, 38, 0.4);
|
||||
}
|
||||
|
||||
.stop-button.disabled {
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
}
|
||||
|
||||
@keyframes border-pulse {
|
||||
0%, 100% { border-color: var(--error); }
|
||||
50% { border-color: #e88; }
|
||||
0%, 100% { border-color: #ef4444; }
|
||||
50% { border-color: #fca5a5; }
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -161,8 +161,8 @@
|
|||
}
|
||||
|
||||
@keyframes glow {
|
||||
0%, 100% { box-shadow: 0 -2px 10px rgba(218, 68, 83, 0.2); }
|
||||
50% { box-shadow: 0 -2px 20px rgba(218, 68, 83, 0.4); }
|
||||
0%, 100% { box-shadow: 0 -2px 10px rgba(233, 69, 96, 0.3); }
|
||||
50% { box-shadow: 0 -2px 20px rgba(233, 69, 96, 0.6); }
|
||||
}
|
||||
|
||||
.footer-stats {
|
||||
|
|
|
|||
|
|
@ -1,174 +1,96 @@
|
|||
<script lang="ts">
|
||||
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
|
||||
import SessionList from '$lib/components/SessionList.svelte';
|
||||
import ChatPanel from '$lib/components/ChatPanel.svelte';
|
||||
import ActivityPanel from '$lib/components/ActivityPanel.svelte';
|
||||
import AgentView from '$lib/components/AgentView.svelte';
|
||||
import MemoryPanel from '$lib/components/MemoryPanel.svelte';
|
||||
import AuditLog from '$lib/components/AuditLog.svelte';
|
||||
import GuardRailsPanel from '$lib/components/GuardRailsPanel.svelte';
|
||||
|
||||
let activeMiddleTab = 'activity';
|
||||
let activeRightTab = 'agents';
|
||||
|
||||
const middleTabs = [
|
||||
{ id: 'activity', label: 'Aktivität', icon: '📋' },
|
||||
{ id: 'memory', label: 'Gedächtnis', icon: '🧠' },
|
||||
{ id: 'audit', label: 'Historie', icon: '📝' },
|
||||
];
|
||||
|
||||
const rightTabs = [
|
||||
{ id: 'agents', label: 'Agents', icon: '🤖' },
|
||||
{ id: 'guards', label: 'Guard-Rails', icon: '🛡️' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<PaneGroup direction="horizontal" autoSaveId="claude-desktop-panels" class="pane-group">
|
||||
<!-- Sessions -->
|
||||
<Pane defaultSize={15} minSize={8} maxSize={30} class="panel">
|
||||
<SessionList />
|
||||
</Pane>
|
||||
|
||||
<PaneResizer class="resizer">
|
||||
<div class="resizer-line"></div>
|
||||
</PaneResizer>
|
||||
|
||||
<!-- Chat -->
|
||||
<Pane defaultSize={35} minSize={15} class="panel">
|
||||
<div class="panels">
|
||||
<!-- Linkes Panel: Chat -->
|
||||
<section class="panel panel-chat">
|
||||
<ChatPanel />
|
||||
</Pane>
|
||||
</section>
|
||||
|
||||
<PaneResizer class="resizer">
|
||||
<div class="resizer-line"></div>
|
||||
</PaneResizer>
|
||||
|
||||
<!-- Aktivität / Memory / Audit -->
|
||||
<Pane defaultSize={25} minSize={10} class="panel">
|
||||
<!-- Mittleres Panel: Aktivität + Agents -->
|
||||
<section class="panel panel-activity">
|
||||
<div class="panel-tabs">
|
||||
{#each middleTabs as tab}
|
||||
<button
|
||||
class="tab"
|
||||
class:active={activeMiddleTab === tab.id}
|
||||
on:click={() => activeMiddleTab = tab.id}
|
||||
>
|
||||
{tab.icon} {tab.label}
|
||||
</button>
|
||||
{/each}
|
||||
<button class="tab active">📋 Aktivität</button>
|
||||
<button class="tab">🤖 Agents</button>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
{#if activeMiddleTab === 'activity'}
|
||||
<ActivityPanel />
|
||||
{:else if activeMiddleTab === 'memory'}
|
||||
<MemoryPanel />
|
||||
{:else if activeMiddleTab === 'audit'}
|
||||
<AuditLog />
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
</section>
|
||||
|
||||
<PaneResizer class="resizer">
|
||||
<div class="resizer-line"></div>
|
||||
</PaneResizer>
|
||||
|
||||
<!-- Agents / Guard-Rails -->
|
||||
<Pane defaultSize={25} minSize={10} class="panel">
|
||||
<div class="panel-tabs">
|
||||
{#each rightTabs as tab}
|
||||
<button
|
||||
class="tab"
|
||||
class:active={activeRightTab === tab.id}
|
||||
on:click={() => activeRightTab = tab.id}
|
||||
>
|
||||
{tab.icon} {tab.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
{#if activeRightTab === 'agents'}
|
||||
<!-- Rechtes Panel: Agent-Details / Präsentation -->
|
||||
<section class="panel panel-details">
|
||||
<AgentView />
|
||||
{:else if activeRightTab === 'guards'}
|
||||
<GuardRailsPanel />
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
</Pane>
|
||||
</PaneGroup>
|
||||
|
||||
<style>
|
||||
/* PaneForge Container */
|
||||
:global(.pane-group) {
|
||||
.panels {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 1px;
|
||||
height: 100%;
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
:global(.panel) {
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-primary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Resizer Handle */
|
||||
:global(.resizer) {
|
||||
width: 8px;
|
||||
background: var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: col-resize;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
:global(.resizer:hover),
|
||||
:global(.resizer[data-state="drag"]) {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.resizer-line {
|
||||
width: 2px;
|
||||
height: 24px;
|
||||
border-radius: 1px;
|
||||
background: var(--text-secondary);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
:global(.resizer:hover) .resizer-line,
|
||||
:global(.resizer[data-state="drag"]) .resizer-line {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.panel-tabs {
|
||||
display: flex;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
padding: var(--spacing-sm);
|
||||
font-size: 0.75rem;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-hover);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--accent);
|
||||
border-bottom-color: var(--accent);
|
||||
border-bottom: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Responsive: Auf kleineren Bildschirmen stapeln */
|
||||
@media (max-width: 1200px) {
|
||||
.panels {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.panel-details {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.panels {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.panel-activity {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue