Initial Commit: Claude Desktop Grundgerüst

- Tauri 2.0 + SvelteKit Projekt aufgesetzt
- Basis-UI mit 3 Panels (Chat, Aktivität, Präsentation)
- Roter STOPP-Button Footer
- Autonomes Gedächtnis-System (memory.rs)
- Änderungs-Log / Audit Trail (audit.rs)
- Multi-Agent-View Komponenten
- NixOS Entwicklungsumgebung (shell.nix)

Phase 1 abgeschlossen, Claude SDK Integration folgt.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-13 12:16:20 +02:00
commit 2822796c7a
32 changed files with 9297 additions and 0 deletions

29
.gitignore vendored Normal file
View file

@ -0,0 +1,29 @@
# Dependencies
node_modules/
# Build outputs
build/
dist/
.svelte-kit/
# Rust/Tauri
src-tauri/target/
src-tauri/gen/
# IDE
.vscode/
.idea/
# Logs
*.log
# Environment
.env
.env.local
# OS
.DS_Store
Thumbs.db
# Package lock (use npm ci)
package-lock.json

352
README.md Normal file
View file

@ -0,0 +1,352 @@
# Claude Desktop — Nativer AI-Assistent
Eigenständige Desktop-Anwendung die Claude Code als Backend nutzt, mit nativem UI, Live-Übersicht und kontrolliertem OS-Zugriff.
## Motivation
Claude Code in VSCodium funktioniert, hat aber Grenzen:
- Sidebar-Chat ist eng, keine eigene Fensterverwaltung
- Kein Überblick was Claude gerade tut (Dateien, Befehle, DB-Queries)
- Kein "Stopp"-Button bei laufenden Aktionen
- Keine Präsentations-Ansicht für Ergebnisse
- Keine native OS-Integration (Fenster steuern, Programme öffnen)
## Vision
### Variante A: Native Desktop-App (begleitend)
Claude arbeitet begleitend — der User sieht alles mit und kann jederzeit eingreifen.
```
┌─────────────────────────────────────────────────────┐
│ Claude Desktop [─][□][×]│
├──────────────┬──────────────────────────────────────┤
│ │ │
│ 💬 Chat │ 📋 Live-Aktivität │
│ │ │
│ Du: Fixe │ ▶ Lese product/price.php:1609 │
│ den Bug in │ ▶ Grep "addMoreActions" in 3 Files │
│ der Preis- │ ▶ Edit actions_produktkarte.class.php│
│ seite │ ✓ Deploy nach /var/www/dolibarr/... │
│ │ │
│ Claude: │ ⚠ Will deployen auf PROD │
│ Gefunden, │ [Erlauben] [Ablehnen] │
│ der Hook... │ │
│ ├──────────────────────────────────────┤
│ │ 📊 Ergebnis-Präsentation │
│ │ │
│ │ Vorher: list=0 (unsichtbar) │
│ │ Nachher: list=3 (auf Karte) │
│ │ │
│ │ [Diff anzeigen] [Screenshot] │
│ │ │
├──────────────┴──────────────────────────────────────┤
│ [⏹ STOPP] CPU: 2% │ DB: claude │ Git: main │
└─────────────────────────────────────────────────────┘
```
**Kernfeatures:**
- **Chat-Panel** — Aufgaben eingeben, Antworten lesen
- **Live-Aktivität** — Echtzeit was Claude tut (Dateien, Befehle, Queries)
- **Kritische Aktionen** — Popup bei Prod-Deploy, DB-Änderungen, Git Push
- **Präsentations-View** — Nach einer Aufgabe: Vorher/Nachher, Diffs, Screenshots
- **STOPP-Button** — Sofort alles abbrechen
- **Statusleiste** — Aktive DB, Git-Branch, CPU/RAM
**Technologie-Stack:**
- **Tauri 2.0** (Rust + WebView) — native App, ~5 MB statt 200 MB Electron
- **SvelteKit** — Frontend (gleiche Technologie wie Leckerbuch, VDE Katalog)
- **Claude Code SDK** (`@anthropic-ai/claude-code`) — AI-Backend
- **Claude DB** — Direkte MySQL-Anbindung (kein REST-Umweg)
- **MCP-Tools** — Docker, Forgejo, Wissensbasis
### Variante B: Autonome VM (selbständig arbeitend)
Claude hat einen eigenen Rechner (VM) mit Desktop und arbeitet Aufgaben selbständig ab. Der User überwacht remote.
```
┌──────────────────────────────────────────┐
│ Unraid Server │
│ │
│ ┌────────────────────────────────────┐ │
│ │ VM: Claude Agent │ │
│ │ │ │
│ │ ┌──────────┐ ┌───────────────┐ │ │
│ │ │ Desktop │ │ Claude Agent │ │ │
│ │ │ (XFCE) │←→│ Computer Use │ │ │
│ │ │ │ │ Terminal │ │ │
│ │ │ Dolibarr │ │ Claude DB │ │ │
│ │ │ Browser │ │ MCP Tools │ │ │
│ │ │ IDE │ │ Git/Forgejo │ │ │
│ │ └──────────┘ └───────────────┘ │ │
│ │ ↕ │ │
│ │ VNC/noVNC (Port 6080) │ │
│ └────────────────────────────────────┘ │
│ │
│ Netzwerk: ISOLIERT von Prod! │
│ - Eigenes VLAN / Bridge │
│ - Zugriff nur auf Test-DB │
│ - Kein SSH zu Unraid │
│ - Kein Zugriff auf Prod-Dolibarr │
└──────────────────────────────────────────┘
Eddy (Browser → VNC)
Sieht Claude arbeiten
Kann jederzeit eingreifen
```
**Wie Claude Computer Use funktioniert:**
1. Claude bekommt einen Screenshot des Desktops
2. Claude analysiert was er sieht
3. Claude sendet Maus-/Tastatur-Befehle
4. Nächster Screenshot → nächste Aktion
5. Kann jedes Programm bedienen das ein Mensch bedienen kann
**Sicherheitskonzept für die VM:**
- **Netzwerk-Isolation** — eigenes VLAN, kein Zugriff auf Prod-Server
- **Nur Test-DB** — dolibarr_test auf 192.168.155.11, nie Prod-DB
- **Kein SSH nach außen** — keine SSH-Keys zu Unraid oder anderen Servern
- **Snapshot-basiert** — VM-Snapshot vor jeder Aufgabe, Rollback bei Problemen
- **Audit-Log** — jeder Befehl, jede Aktion wird geloggt
- **Zeitlimit** — maximale Laufzeit pro Aufgabe
- **Kill-Switch** — ein Befehl stoppt alles sofort
**Use Cases für die VM:**
- Dolibarr-Module entwickeln und testen (Browser + Terminal)
- Automatische Code-Reviews über mehrere Repos
- Dokumentation erstellen mit Screenshots
- UI-Tests durchführen (Dolibarr durchklicken, Fehler finden)
- Batch-Aufgaben (alle Module updaten, Lang-Dateien synchronisieren)
## Empfehlung: Stufenweise vorgehen
### Stufe 1: Native Desktop-App (Variante A)
- Sofort umsetzbar mit vorhandenem Wissen (Svelte, Tauri)
- Claude arbeitet begleitend, User behält Kontrolle
- Ersetzt die VSCodium-Sidebar durch bessere UX
- Geschätzter Aufwand: 2-3 Wochen Grundgerüst
### Stufe 2: Autonome VM (Variante B) — später
- Erst wenn Stufe 1 stabil läuft und Vertrauen aufgebaut ist
- Guard-Rails und Audit-Log müssen wasserdicht sein
- Netzwerk-Isolation auf Unraid einrichten
- Geschätzter Aufwand: 1-2 Wochen Setup, dann iterativ
## Architektur — Variante A im Detail
### Projektstruktur
```
ClaudeDesktop/
├── src-tauri/ # Rust-Backend (Tauri)
│ ├── src/
│ │ ├── main.rs # App-Einstiegspunkt
│ │ ├── claude.rs # Claude SDK Integration
│ │ ├── db.rs # Direkte MySQL-Anbindung
│ │ └── guard.rs # Sicherheits-Regeln
│ ├── Cargo.toml
│ └── tauri.conf.json
├── src/ # SvelteKit Frontend
│ ├── routes/
│ │ ├── +layout.svelte # Haupt-Layout (Chat + Panels)
│ │ ├── chat/ # Chat-Ansicht
│ │ ├── activity/ # Live-Aktivität
│ │ ├── presentation/ # Ergebnis-Präsentation
│ │ └── settings/ # Einstellungen
│ ├── lib/
│ │ ├── claude-bridge.ts # Kommunikation mit Tauri-Backend
│ │ ├── db.ts # Claude-DB Queries
│ │ └── stores.ts # Svelte Stores (State)
│ └── app.html
├── package.json
├── svelte.config.js
├── vite.config.ts
└── README.md
```
### Datenfluss
```
User-Eingabe (Chat)
SvelteKit Frontend
↓ (Tauri IPC)
Rust Backend
Claude Code SDK (Node.js child process)
Claude API (Anthropic)
Tool-Ausführung (Bash, Dateien, DB, MCP)
Ergebnis zurück an Frontend
Live-Aktivität + Präsentation anzeigen
```
### Claude-DB Integration
Statt REST-API (aktuell) → direkte MySQL-Verbindung im Rust-Backend:
```rust
// Direkte DB-Abfrage, kein MCP-Umweg
let results = db.query(
"SELECT * FROM knowledge WHERE MATCH(title,content) AGAINST(? IN BOOLEAN MODE)",
&[search_term]
).await?;
```
- Schneller (kein HTTP-Roundtrip)
- Keine Token-Limits bei großen Ergebnissen
- Full-Text-Search direkt in MySQL
### Guard-Rails im nativen Programm
```rust
enum ActionRisk {
Safe, // Dateien lesen, Code schreiben → automatisch
Moderate, // Git commit, lokaler Deploy → Statusbar-Hinweis
Critical, // Prod-Deploy, DB-Schema, Git Push → Popup + Bestätigung
Blocked, // rm -rf, force push main → hart blockiert
}
```
## Sprach-Interface — Reden mit Claude
### Konzept
Echtes Gespräch mit Claude — reden, unterbrechen, weiterreden. Kein "Aufnahme starten/stoppen", sondern natürlicher Dialog.
```
┌─────────────────────────────────────────────────┐
│ │
│ 🎤 Du sprichst │
│ ↓ │
│ Whisper (Speech-to-Text, lokal) │
│ ↓ │
│ VAD erkennt: "User hat aufgehört zu reden" │
│ ↓ │
│ Text → Claude API → Antwort-Text │
│ ↓ │
│ TTS (Text-to-Speech) → Lautsprecher 🔊 │
│ ↓ │
│ Du unterbrichst → VAD erkennt Sprache │
│ → TTS stoppt sofort │
│ → Whisper nimmt deine neue Eingabe auf │
│ → Kreislauf beginnt von vorn │
│ │
└─────────────────────────────────────────────────┘
```
### Technologie
| Komponente | Technologie | Läuft wo |
|---|---|---|
| **Speech-to-Text** | OpenAI Whisper (whisper.cpp) | Lokal auf NixOS, kein Cloud-Upload |
| **Voice Activity Detection** | Silero VAD oder WebRTC VAD | Lokal, erkennt Sprache vs. Stille |
| **Text-to-Speech** | OpenAI TTS API oder ElevenLabs | Cloud (Streaming) |
| **Interrupt-Erkennung** | VAD + sofortiger TTS-Stopp | Lokal |
### Gesprächs-Modi
**Freies Gespräch** — wie mit einem Kollegen reden:
- Du redest, Claude hört zu (Whisper transkribiert live)
- Pause > 1,5 Sekunden → Claude antwortet
- Du unterbrichst → Claude stoppt sofort, hört dir zu
- Claude kann nachfragen wenn etwas unklar ist
**Diktier-Modus** — Claude führt aus was du sagst:
- "Fixe den Bug in der Preisseite, der Umrechnungsfaktor wird nicht berücksichtigt"
- Claude arbeitet, kommentiert per Sprache was er tut
- Du kannst jederzeit "Stopp" oder "Warte mal" sagen
**Präsentations-Modus** — Claude erklärt was er gemacht hat:
- "Zeig mir was du geändert hast"
- Claude öffnet Diff-View und erklärt per Sprache die Änderungen
- Du kannst zwischenfragen: "Warum hast du das so gemacht?"
### Stimmen-Optionen
**OpenAI TTS API:**
- 6 Stimmen (alloy, echo, fable, onyx, nova, shimmer)
- Sehr natürlich, Streaming-fähig (~200ms Latenz)
- Kosten: ~$15 pro 1M Zeichen
**ElevenLabs:**
- Hunderte Stimmen, eigene Stimmen klonbar
- Noch natürlicher, emotionaler
- Deutsch-Support gut
- Kosten: ab $5/Monat (30 Min)
**Lokal (Piper TTS):**
- Kostenlos, keine Cloud
- Deutsche Stimmen verfügbar
- Qualität gut aber nicht so natürlich wie Cloud
- Keine Latenz durch Netzwerk
### Latenz-Budget (Ziel: < 2 Sekunden)
```
VAD erkennt Stille: ~300ms
Whisper transkribiert: ~500ms (lokal, whisper.cpp mit GPU)
Claude API Antwort: ~800ms (erstes Token, Streaming)
TTS erstes Audio: ~200ms (Streaming)
─────────────────────────────────
Gesamt bis erste Silbe: ~1.800ms
```
Mit Streaming: Claude beginnt zu "reden" während er noch denkt — wie ein Mensch der anfängt zu antworten bevor der Gedanke fertig ist.
### Integration in die Desktop-App
```
┌─────────────────────────────────────────────────────┐
│ Claude Desktop [─][□][×]│
├──────────────┬──────────────────────────────────────┤
│ │ │
│ 💬 Chat │ 📋 Live-Aktivität │
│ │ │
│ (Text wird │ ▶ Lese product/price.php │
│ live mit- │ ▶ Edit actions_produktkarte.class.php│
│ geschrieben │ ✓ Deploy nach /var/www/dolibarr/... │
│ während │ │
│ gesprochen) │ │
│ │ │
├──────────────┴──────────────────────────────────────┤
│ 🎤 ████████░░░░░░ Zuhören... [🔇 Stumm] [⏹] │
└─────────────────────────────────────────────────────┘
```
- Mikrofon-Leiste unten zeigt Pegel
- Gesprochenes wird als Text im Chat mitgeschrieben (Transkript)
- Claudes Antwort wird gleichzeitig als Text angezeigt und vorgelesen
- Stumm-Taste schaltet Mikrofon aus (nur Text-Modus)
### Whisper lokal auf NixOS
```nix
# In configuration.nix
environment.systemPackages = with pkgs; [
whisper-cpp # C++ Port, schnell, CPU/GPU
# oder
openai-whisper # Original Python, braucht mehr RAM
];
```
Whisper "small" oder "medium" Modell reicht für Deutsch — ~500 MB RAM, Echtzeit auf CPU.
## Voraussetzungen
### Für Variante A (Desktop-App)
- NixOS: `rustc`, `cargo`, `nodejs`, `tauri-cli` in der Nix-Config
- Anthropic API Key (bereits vorhanden)
- Claude Code SDK npm-Paket
- MySQL-Client-Library für Rust (`sqlx` oder `mysql_async`)
### Für Variante B (VM)
- Unraid: VM mit Linux + Desktop (Ubuntu/Debian + XFCE)
- VNC-Server in der VM
- noVNC-Container auf Unraid für Browser-Zugriff
- Isoliertes Netzwerk (VLAN oder Bridge)
- Claude API Key + Computer Use Beta-Zugang
## Offene Fragen
- [ ] Anthropic API Key Kosten für Computer Use (Screenshot-intensiv)?
- [ ] Tauri 2.0 auf NixOS — Nix-Paket verfügbar?
- [ ] Claude Code SDK — stabil genug für Production?
- [ ] VM auf Unraid — genug RAM/CPU frei?

29
package.json Normal file
View file

@ -0,0 +1,29 @@
{
"name": "claude-desktop",
"version": "0.1.0",
"description": "Native Desktop-App für Claude Code mit Live-Übersicht, Guard-Rails und Sprach-Interface",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@tauri-apps/cli": "^2.0.0",
"svelte": "^5.0.0",
"typescript": "^5.0.0",
"vite": "^5.0.0"
},
"dependencies": {
"@anthropic-ai/claude-code": "^0.2.0",
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-shell": "^2.0.0"
}
}

66
shell.nix Normal file
View file

@ -0,0 +1,66 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = with pkgs; [
# Rust (wir nutzen rustup, aber brauchen den Linker)
gcc
pkg-config
openssl
# Tauri-Abhängigkeiten für Linux
webkitgtk_4_1
libappindicator-gtk3
librsvg
# GTK/GLib für Tauri
gtk3
glib
cairo
pango
gdk-pixbuf
# Zusätzliche Abhängigkeiten
libsoup_3
at-spi2-atk
# Node.js (falls nicht global)
nodejs_22
# Für Audio (Whisper/TTS später)
alsa-lib
ffmpeg
# Zusätzliche Bibliotheken für Tauri CLI
bzip2
zlib
xz
zstd
];
# Umgebungsvariablen für Rust/Tauri
shellHook = ''
export PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig:$PKG_CONFIG_PATH"
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [
pkgs.webkitgtk_4_1
pkgs.libappindicator-gtk3
pkgs.gtk3
pkgs.cairo
pkgs.pango
pkgs.gdk-pixbuf
pkgs.librsvg
pkgs.libsoup_3
pkgs.bzip2
pkgs.zlib
pkgs.xz
pkgs.zstd
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 " Node: $(node --version 2>/dev/null || echo 'nicht gefunden')"
'';
}

5186
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

29
src-tauri/Cargo.toml Normal file
View file

@ -0,0 +1,29 @@
[package]
name = "claude-desktop"
version = "0.1.0"
description = "Native Desktop-App für Claude Code"
authors = ["Eddy <eddy@alles-watt-laeuft.de>"]
edition = "2021"
[lib]
name = "claude_desktop_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4", "serde"] }
rusqlite = { version = "0.31", features = ["bundled"] }
[profile.release]
panic = "abort"
codegen-units = 1
lto = true
strip = true

3
src-tauri/build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

179
src-tauri/src/audit.rs Normal file
View file

@ -0,0 +1,179 @@
// Claude Desktop — Änderungs-Log (Audit Trail)
// Protokolliert alle Änderungen an Einstellungen, Guard-Rails, Hooks, Skills, etc.
use serde::{Deserialize, Serialize};
use tauri::AppHandle;
/// Kategorie der Änderung
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AuditCategory {
GuardRail, // Freigabe-Regeln
Pattern, // Vorgehensweisen
Hook, // Claude Hooks
Skill, // Claude Skills
Setting, // App-Einstellungen
MCP, // MCP-Server Konfiguration
Memory, // Gedächtnis-Einträge
}
/// Art der Aktion
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AuditAction {
Create,
Update,
Delete,
Enable,
Disable,
}
/// Ein Audit-Eintrag
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEntry {
pub id: String,
pub timestamp: String,
pub category: AuditCategory,
pub action: AuditAction,
pub item_id: String,
pub item_name: String,
pub old_value: Option<serde_json::Value>,
pub new_value: Option<serde_json::Value>,
pub reason: Option<String>,
pub auto_corrected: bool,
pub session_id: Option<String>,
}
/// Audit-Log Manager
#[derive(Debug, Default)]
pub struct AuditLog {
entries: Vec<AuditEntry>,
}
impl AuditLog {
pub fn new() -> Self {
Self { entries: vec![] }
}
/// Fügt einen Eintrag hinzu
pub fn log(&mut self, entry: AuditEntry) {
println!(
"📋 Audit: {:?} {:?} - {} ({})",
entry.action, entry.category, entry.item_name,
entry.reason.as_deref().unwrap_or("keine Begründung")
);
self.entries.push(entry);
}
/// Holt die letzten N Einträge
pub fn recent(&self, limit: usize) -> Vec<&AuditEntry> {
self.entries.iter().rev().take(limit).collect()
}
/// Holt Einträge nach Kategorie
pub fn by_category(&self, category: &AuditCategory) -> Vec<&AuditEntry> {
self.entries
.iter()
.filter(|e| std::mem::discriminant(&e.category) == std::mem::discriminant(category))
.collect()
}
/// Holt auto-korrigierte Einträge
pub fn auto_corrected(&self) -> Vec<&AuditEntry> {
self.entries.iter().filter(|e| e.auto_corrected).collect()
}
/// Statistiken
pub fn stats(&self) -> AuditStats {
AuditStats {
total: self.entries.len(),
auto_corrected: self.entries.iter().filter(|e| e.auto_corrected).count(),
today: self.entries.iter().filter(|e| {
// Vereinfachte Prüfung - in echt: Datum vergleichen
e.timestamp.starts_with(&chrono::Local::now().format("%Y-%m-%d").to_string())
}).count(),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AuditStats {
pub total: usize,
pub auto_corrected: usize,
pub today: usize,
}
// Tauri-Commands
/// Holt die letzten Audit-Einträge
#[tauri::command]
pub async fn get_audit_log(limit: Option<usize>) -> Result<Vec<AuditEntry>, String> {
let limit = limit.unwrap_or(50);
// TODO: Aus SQLite laden
Ok(vec![])
}
/// Fügt einen Audit-Eintrag hinzu
#[tauri::command]
pub async fn add_audit_entry(
category: String,
action: String,
item_id: String,
item_name: String,
old_value: Option<serde_json::Value>,
new_value: Option<serde_json::Value>,
reason: Option<String>,
auto_corrected: bool,
) -> Result<(), String> {
let category = match category.as_str() {
"guard_rail" => AuditCategory::GuardRail,
"pattern" => AuditCategory::Pattern,
"hook" => AuditCategory::Hook,
"skill" => AuditCategory::Skill,
"setting" => AuditCategory::Setting,
"mcp" => AuditCategory::MCP,
"memory" => AuditCategory::Memory,
_ => return Err(format!("Unbekannte Kategorie: {}", category)),
};
let action = match action.as_str() {
"create" => AuditAction::Create,
"update" => AuditAction::Update,
"delete" => AuditAction::Delete,
"enable" => AuditAction::Enable,
"disable" => AuditAction::Disable,
_ => return Err(format!("Unbekannte Aktion: {}", action)),
};
let entry = AuditEntry {
id: uuid::Uuid::new_v4().to_string(),
timestamp: chrono::Local::now().to_rfc3339(),
category,
action,
item_id,
item_name,
old_value,
new_value,
reason,
auto_corrected,
session_id: None,
};
// TODO: In SQLite speichern
println!("📋 Audit-Eintrag hinzugefügt: {:?}", entry);
Ok(())
}
/// Holt Audit-Statistiken
#[tauri::command]
pub async fn get_audit_stats() -> Result<AuditStats, String> {
// TODO: Echte Implementierung
Ok(AuditStats {
total: 0,
auto_corrected: 0,
today: 0,
})
}

50
src-tauri/src/claude.rs Normal file
View file

@ -0,0 +1,50 @@
// Claude Desktop — Claude SDK Integration
// Kommunikation mit Claude Code via Node.js Child-Process
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter};
/// Status eines Agents
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentStatus {
pub id: String,
pub agent_type: String,
pub status: String,
pub task: String,
pub tool_calls: u32,
}
/// Nachricht an Claude senden
#[tauri::command]
pub async fn send_message(
app: AppHandle,
message: String,
) -> Result<String, String> {
println!("📨 Nachricht empfangen: {}", &message[..message.len().min(50)]);
// TODO: Claude SDK über Node.js Child-Process aufrufen
// Vorläufig: Placeholder-Antwort
Ok("Claude SDK noch nicht verbunden. Integration folgt in Phase 1.3.".to_string())
}
/// Alle Agents stoppen
#[tauri::command]
pub async fn stop_all_agents(app: AppHandle) -> Result<(), String> {
println!("⏹️ STOPP: Alle Agents werden gestoppt");
// TODO: AbortController für alle laufenden Prozesse triggern
// Event an Frontend senden
app.emit("agents-stopped", ()).map_err(|e| e.to_string())?;
Ok(())
}
/// Status aller Agents abrufen
#[tauri::command]
pub async fn get_agent_status() -> Result<Vec<AgentStatus>, String> {
// TODO: Echte Agent-Daten zurückgeben
Ok(vec![])
}

45
src-tauri/src/lib.rs Normal file
View file

@ -0,0 +1,45 @@
// Claude Desktop — Tauri Backend
// Hauptmodul für die Rust-Seite der App
use tauri::Manager;
mod claude;
mod memory;
mod audit;
/// Initialisiert die App
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![
// Claude SDK
claude::send_message,
claude::stop_all_agents,
claude::get_agent_status,
// Gedächtnis-System
memory::load_memory,
memory::get_sticky_context,
memory::save_pattern,
memory::detect_issue,
// Audit-Log
audit::get_audit_log,
audit::add_audit_entry,
audit::get_audit_stats,
])
.setup(|app| {
let handle = app.handle().clone();
println!("🤖 Claude Desktop gestartet");
// Gedächtnis-System beim Start laden
tauri::async_runtime::spawn(async move {
println!("🧠 Initialisiere Gedächtnis-System...");
// TODO: memory::load_memory aufrufen
});
Ok(())
})
.run(tauri::generate_context!())
.expect("Fehler beim Starten der App");
}

6
src-tauri/src/main.rs Normal file
View file

@ -0,0 +1,6 @@
// Verhindert CMD-Fenster auf Windows bei Release-Build
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
claude_desktop_lib::run()
}

164
src-tauri/src/memory.rs Normal file
View file

@ -0,0 +1,164 @@
// Claude Desktop — Autonomes Gedächtnis-System
// Lädt Zugänge, Patterns, Vorgehensweisen automatisch und behält sie im Kontext
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tauri::AppHandle;
/// Kategorien für Sticky Context (werden nie vergessen)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ContextCategory {
Critical, // API-Keys, Server-URLs, Projekt-Pfade
Pattern, // Bekannte Fehler und Workarounds
Preference, // Benutzer-Präferenzen
GuardRail, // Freigabe-Regeln
Hook, // Aktive Hooks
Skill, // Verfügbare Skills
}
/// Ein Eintrag im Gedächtnis
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryEntry {
pub id: String,
pub category: ContextCategory,
pub key: String,
pub value: serde_json::Value,
pub sticky: bool, // Wird nie durch Compacting entfernt
pub auto_load: bool, // Wird beim Start automatisch geladen
pub last_used: Option<String>,
pub use_count: u32,
}
/// Das Gedächtnis-System
#[derive(Debug, Default)]
pub struct MemorySystem {
entries: HashMap<String, MemoryEntry>,
loaded_from_db: bool,
}
impl MemorySystem {
pub fn new() -> Self {
Self {
entries: HashMap::new(),
loaded_from_db: false,
}
}
/// Fügt einen Eintrag hinzu
pub fn add(&mut self, entry: MemoryEntry) {
self.entries.insert(entry.id.clone(), entry);
}
/// Holt einen Eintrag
pub fn get(&self, id: &str) -> Option<&MemoryEntry> {
self.entries.get(id)
}
/// Holt alle Einträge einer Kategorie
pub fn get_by_category(&self, category: ContextCategory) -> Vec<&MemoryEntry> {
self.entries
.values()
.filter(|e| e.category == category)
.collect()
}
/// Holt alle Sticky-Einträge (für Kontext-Injection)
pub fn get_sticky_context(&self) -> Vec<&MemoryEntry> {
self.entries.values().filter(|e| e.sticky).collect()
}
/// Holt alle Auto-Load-Einträge
pub fn get_auto_load(&self) -> Vec<&MemoryEntry> {
self.entries.values().filter(|e| e.auto_load).collect()
}
/// Statistiken
pub fn stats(&self) -> MemoryStats {
MemoryStats {
total: self.entries.len(),
sticky: self.entries.values().filter(|e| e.sticky).count(),
by_category: self.count_by_category(),
}
}
fn count_by_category(&self) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for entry in self.entries.values() {
let cat = format!("{:?}", entry.category);
*counts.entry(cat).or_insert(0) += 1;
}
counts
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MemoryStats {
pub total: usize,
pub sticky: usize,
pub by_category: HashMap<String, usize>,
}
/// Vorgehensweise / Pattern
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pattern {
pub id: String,
pub name: String,
pub description: String,
pub trigger: String, // Wann wird es angewendet
pub old_approach: String, // Was wurde vorher gemacht
pub new_approach: String, // Was ist die Korrektur
pub reason: String, // Warum wurde korrigiert
pub occurrence_count: u32, // Wie oft ist das Problem aufgetreten
pub auto_corrected: bool, // Wurde automatisch korrigiert
pub created_at: String,
pub updated_at: String,
}
// Tauri-Commands
/// Lädt das Gedächtnis beim Start
#[tauri::command]
pub async fn load_memory(app: AppHandle) -> Result<MemoryStats, String> {
println!("🧠 Lade Gedächtnis-System...");
// TODO: Aus lokaler SQLite laden
// TODO: Mit Remote claude-db synchronisieren
// Placeholder-Statistiken
Ok(MemoryStats {
total: 0,
sticky: 0,
by_category: HashMap::new(),
})
}
/// Holt den Sticky-Kontext für Claude
#[tauri::command]
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(pattern: Pattern) -> Result<(), String> {
println!("📝 Speichere Vorgehensweise: {}", pattern.name);
// 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(
error_message: String,
context: String,
) -> Result<Option<Pattern>, String> {
println!("🔍 Prüfe auf bekannte Probleme: {}", &error_message[..error_message.len().min(50)]);
// TODO: Pattern-Matching gegen bekannte Probleme
Ok(None)
}

52
src-tauri/tauri.conf.json Normal file
View file

@ -0,0 +1,52 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Claude Desktop",
"identifier": "de.alles-watt-laeuft.claude-desktop",
"version": "0.1.0",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../build"
},
"app": {
"windows": [
{
"title": "Claude Desktop",
"width": 1400,
"height": 900,
"minWidth": 800,
"minHeight": 600,
"resizable": true,
"fullscreen": false,
"decorations": true,
"transparent": false,
"center": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": ["appimage", "deb"]
},
"plugins": {
"shell": {
"open": true,
"scope": [
{
"name": "node",
"cmd": "node",
"args": true
},
{
"name": "npm",
"cmd": "npm",
"args": true
}
]
}
}
}

139
src/app.css Normal file
View file

@ -0,0 +1,139 @@
/* Claude Desktop — Basis-Styles */
:root {
/* 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;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
/* Border-Radius */
--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 6px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
/* Font */
--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;
}
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
font-family: var(--font-sans);
background: var(--bg-primary);
color: var(--text-primary);
overflow: hidden;
}
/* Scrollbar-Styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent);
}
/* Button-Reset */
button {
font-family: inherit;
cursor: pointer;
border: none;
background: none;
color: inherit;
}
/* Input-Reset */
input, textarea {
font-family: inherit;
border: none;
background: var(--bg-secondary);
color: var(--text-primary);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
}
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-secondary);
border-radius: var(--radius-sm);
}
code {
padding: 0.1em 0.3em;
}
pre {
padding: var(--spacing-md);
overflow-x: auto;
}
/* Animationen */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.animate-pulse {
animation: pulse 2s ease-in-out infinite;
}
.animate-spin {
animation: spin 1s linear infinite;
}

13
src/app.html Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Claude Desktop</title>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View file

@ -0,0 +1,166 @@
<script lang="ts">
import { recentToolCalls, agents } from '$lib/stores/app';
// Tool-Icons
const toolIcons: Record<string, string> = {
Read: '📖',
Write: '✏️',
Edit: '✏️',
Bash: '🖥️',
Grep: '🔍',
Glob: '📂',
Task: '🤖',
WebFetch: '🌐',
WebSearch: '🔎'
};
function getToolIcon(tool: string): string {
return toolIcons[tool] || '🔧';
}
function formatTime(date: Date): string {
return date.toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
function getAgentName(agentId: string): string {
const agent = $agents.find((a) => a.id === agentId);
if (!agent) return 'Main';
return agent.type.charAt(0).toUpperCase() + agent.type.slice(1);
}
function formatArgs(args: Record<string, unknown>): string {
// Versuche sinnvolle Info zu extrahieren
if (args.file_path) return String(args.file_path).split('/').pop() || '';
if (args.pattern) return `"${args.pattern}"`;
if (args.command) {
const cmd = String(args.command);
return cmd.length > 30 ? cmd.substring(0, 30) + '...' : cmd;
}
if (args.url) return String(args.url);
return '';
}
</script>
<div class="activity-panel">
{#if $recentToolCalls.length === 0}
<div class="empty-state">
<p>Noch keine Aktivität.</p>
<p class="hint">Tool-Aufrufe erscheinen hier in Echtzeit.</p>
</div>
{:else}
<div class="activity-list">
{#each $recentToolCalls as call}
<div class="activity-item" class:running={call.status === 'running'} class:failed={call.status === 'failed'}>
<span class="activity-time">{formatTime(call.startedAt)}</span>
<span class="activity-icon">{getToolIcon(call.tool)}</span>
<span class="activity-agent">{getAgentName(call.agentId)}</span>
<span class="activity-tool">{call.tool}</span>
<span class="activity-args">{formatArgs(call.args)}</span>
<span class="activity-status">
{#if call.status === 'running'}
<span class="status-dot running"></span>
{:else if call.status === 'completed'}
{:else}
{/if}
</span>
</div>
{/each}
</div>
{/if}
</div>
<style>
.activity-panel {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-secondary);
text-align: center;
padding: var(--spacing-md);
}
.empty-state .hint {
font-size: 0.75rem;
margin-top: var(--spacing-sm);
}
.activity-list {
flex: 1;
overflow-y: auto;
font-family: var(--font-mono);
font-size: 0.75rem;
}
.activity-item {
display: grid;
grid-template-columns: 70px 24px 60px 80px 1fr 24px;
gap: var(--spacing-xs);
align-items: center;
padding: var(--spacing-xs) var(--spacing-sm);
border-bottom: 1px solid var(--bg-secondary);
transition: background 0.2s ease;
}
.activity-item:hover {
background: var(--bg-secondary);
}
.activity-item.running {
background: rgba(74, 222, 128, 0.1);
}
.activity-item.failed {
background: rgba(239, 68, 68, 0.1);
}
.activity-time {
color: var(--text-secondary);
}
.activity-icon {
text-align: center;
}
.activity-agent {
color: var(--accent);
}
.activity-tool {
font-weight: 600;
}
.activity-args {
color: var(--text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.activity-status {
text-align: center;
}
.status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success);
animation: pulse 1s ease-in-out infinite;
}
</style>

View file

@ -0,0 +1,267 @@
<script lang="ts">
import { agents, selectedAgentId, agentCount } from '$lib/stores/app';
import type { Agent } from '$lib/stores/app';
// Status-Icons
const statusIcons: Record<Agent['status'], string> = {
active: '🟢',
waiting: '🟡',
idle: '⚪',
stopped: '🔴'
};
// Typ-Namen
const typeNames: Record<Agent['type'], string> = {
main: 'Main Agent',
explore: 'Explore',
plan: 'Plan',
bash: 'Bash'
};
function selectAgent(id: string) {
$selectedAgentId = $selectedAgentId === id ? null : id;
}
function formatDuration(startedAt: Date): string {
const diff = Date.now() - startedAt.getTime();
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
if (minutes > 0) {
return `${minutes}:${String(seconds % 60).padStart(2, '0')}`;
}
return `${seconds}s`;
}
</script>
<div class="agent-view">
<div class="agent-header">
<h2>🤖 Agents & Sub-Agents</h2>
<div class="agent-summary">
{$agentCount.total} gesamt | {$agentCount.active} aktiv
</div>
</div>
{#if $agents.length === 0}
<div class="empty-state">
<p>Keine Agents aktiv.</p>
<p class="hint">Agents erscheinen hier wenn Claude arbeitet.</p>
</div>
{:else}
<div class="agent-list">
{#each $agents as agent}
<button
class="agent-item"
class:selected={$selectedAgentId === agent.id}
class:active={agent.status === 'active'}
on:click={() => selectAgent(agent.id)}
>
<div class="agent-main">
<span class="agent-status">{statusIcons[agent.status]}</span>
<span class="agent-type">{typeNames[agent.type]}</span>
<span class="agent-duration">({formatDuration(agent.startedAt)})</span>
</div>
<div class="agent-task">{agent.task}</div>
<div class="agent-tools">
Tools: {agent.toolCalls.length} Aufrufe
</div>
</button>
{/each}
</div>
<!-- Detail-Ansicht wenn Agent ausgewählt -->
{#if $selectedAgentId}
{@const selectedAgent = $agents.find((a) => a.id === $selectedAgentId)}
{#if selectedAgent}
<div class="agent-details">
<h3>Details: {typeNames[selectedAgent.type]}</h3>
<div class="detail-row">
<span class="detail-label">Status:</span>
<span class="detail-value">{statusIcons[selectedAgent.status]} {selectedAgent.status}</span>
</div>
<div class="detail-row">
<span class="detail-label">Aufgabe:</span>
<span class="detail-value">{selectedAgent.task}</span>
</div>
<div class="detail-row">
<span class="detail-label">Gestartet:</span>
<span class="detail-value">{selectedAgent.startedAt.toLocaleTimeString('de-DE')}</span>
</div>
<div class="detail-row">
<span class="detail-label">Laufzeit:</span>
<span class="detail-value">{formatDuration(selectedAgent.startedAt)}</span>
</div>
<h4>Tool-Aufrufe ({selectedAgent.toolCalls.length})</h4>
<div class="tool-list">
{#each selectedAgent.toolCalls.slice(-10) as call}
<div class="tool-item">
<span class="tool-name">{call.tool}</span>
<span class="tool-status" class:completed={call.status === 'completed'} class:failed={call.status === 'failed'}>
{call.status}
</span>
</div>
{/each}
</div>
</div>
{/if}
{/if}
{/if}
</div>
<style>
.agent-view {
display: flex;
flex-direction: column;
height: 100%;
}
.agent-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);
}
.agent-header h2 {
font-size: 0.875rem;
font-weight: 600;
}
.agent-summary {
font-size: 0.75rem;
color: var(--text-secondary);
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-secondary);
text-align: center;
padding: var(--spacing-md);
}
.empty-state .hint {
font-size: 0.75rem;
margin-top: var(--spacing-sm);
}
.agent-list {
flex: 1;
overflow-y: auto;
padding: var(--spacing-sm);
}
.agent-item {
width: 100%;
text-align: left;
padding: var(--spacing-sm) var(--spacing-md);
margin-bottom: var(--spacing-xs);
background: var(--bg-secondary);
border: 1px solid var(--bg-tertiary);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
}
.agent-item:hover {
background: var(--bg-tertiary);
}
.agent-item.selected {
border-color: var(--accent);
}
.agent-item.active {
border-left: 3px solid var(--success);
}
.agent-main {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-xs);
}
.agent-type {
font-weight: 600;
}
.agent-duration {
font-size: 0.75rem;
color: var(--text-secondary);
}
.agent-task {
font-size: 0.75rem;
color: var(--text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.agent-tools {
font-size: 0.625rem;
color: var(--text-secondary);
margin-top: var(--spacing-xs);
}
/* Detail-Ansicht */
.agent-details {
padding: var(--spacing-md);
background: var(--bg-secondary);
border-top: 1px solid var(--bg-tertiary);
max-height: 40%;
overflow-y: auto;
}
.agent-details h3 {
font-size: 0.875rem;
margin-bottom: var(--spacing-sm);
}
.agent-details h4 {
font-size: 0.75rem;
margin-top: var(--spacing-md);
margin-bottom: var(--spacing-xs);
}
.detail-row {
display: flex;
gap: var(--spacing-sm);
font-size: 0.75rem;
margin-bottom: var(--spacing-xs);
}
.detail-label {
color: var(--text-secondary);
min-width: 80px;
}
.tool-list {
display: flex;
flex-direction: column;
gap: 2px;
}
.tool-item {
display: flex;
justify-content: space-between;
font-size: 0.625rem;
padding: 2px var(--spacing-xs);
background: var(--bg-primary);
border-radius: var(--radius-sm);
}
.tool-status.completed {
color: var(--success);
}
.tool-status.failed {
color: var(--error);
}
</style>

View file

@ -0,0 +1,299 @@
<script lang="ts">
import { onMount } from 'svelte';
interface AuditEntry {
id: string;
timestamp: string;
category: string;
action: string;
item_name: string;
old_value?: unknown;
new_value?: unknown;
reason?: string;
auto_corrected: boolean;
}
let entries: AuditEntry[] = [];
let filter = 'all';
let loading = true;
// Kategorie-Icons
const categoryIcons: Record<string, string> = {
guard_rail: '🛡️',
pattern: '📋',
hook: '🪝',
skill: '🎯',
setting: '⚙️',
mcp: '🔌',
memory: '🧠'
};
// Aktions-Icons
const actionIcons: Record<string, string> = {
create: '',
update: '✏️',
delete: '🗑️',
enable: '✅',
disable: '❌'
};
onMount(async () => {
// TODO: Echte Daten laden via Tauri
// const result = await invoke('get_audit_log', { limit: 50 });
// 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 {
const date = new Date(timestamp);
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`;
return date.toLocaleDateString('de-DE');
}
function filteredEntries(): AuditEntry[] {
if (filter === 'all') return entries;
if (filter === 'auto') return entries.filter((e) => e.auto_corrected);
return entries.filter((e) => e.category === filter);
}
</script>
<div class="audit-log">
<div class="panel-header">
<h2>📝 Änderungshistorie</h2>
<select bind:value={filter}>
<option value="all">Alle</option>
<option value="auto">Auto-korrigiert</option>
<option value="guard_rail">Guard-Rails</option>
<option value="pattern">Vorgehensweisen</option>
<option value="hook">Hooks</option>
<option value="skill">Skills</option>
<option value="setting">Einstellungen</option>
</select>
</div>
{#if loading}
<div class="loading-state">Lade...</div>
{:else if filteredEntries().length === 0}
<div class="empty-state">Keine Einträge</div>
{:else}
<div class="entries-list">
{#each filteredEntries() as entry}
<div class="entry" class:auto-corrected={entry.auto_corrected}>
<div class="entry-header">
<span class="entry-time">{formatTime(entry.timestamp)}</span>
<span class="entry-category">
{categoryIcons[entry.category] || '📦'}
</span>
<span class="entry-action">
{actionIcons[entry.action] || '•'}
</span>
<span class="entry-name">{entry.item_name}</span>
{#if entry.auto_corrected}
<span class="auto-badge">⚡ Auto</span>
{/if}
</div>
{#if entry.old_value && entry.new_value}
<div class="entry-change">
<span class="old-value">{entry.old_value}</span>
<span class="arrow"></span>
<span class="new-value">{entry.new_value}</span>
</div>
{/if}
{#if entry.reason}
<div class="entry-reason">
💬 {entry.reason}
</div>
{/if}
</div>
{/each}
</div>
{/if}
<div class="actions">
<button class="btn-export">📤 Export</button>
<button class="btn-all">Alle anzeigen</button>
</div>
</div>
<style>
.audit-log {
padding: var(--spacing-md);
height: 100%;
display: flex;
flex-direction: column;
}
.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;
}
.panel-header select {
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-sm);
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--bg-tertiary);
font-size: 0.75rem;
}
.loading-state,
.empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
}
.entries-list {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.entry {
padding: var(--spacing-sm);
background: var(--bg-secondary);
border-radius: var(--radius-md);
border-left: 3px solid var(--bg-tertiary);
}
.entry.auto-corrected {
border-left-color: var(--warning);
}
.entry-header {
display: flex;
align-items: center;
gap: var(--spacing-xs);
font-size: 0.875rem;
}
.entry-time {
color: var(--text-secondary);
font-size: 0.75rem;
min-width: 80px;
}
.entry-category,
.entry-action {
width: 20px;
text-align: center;
}
.entry-name {
flex: 1;
font-weight: 500;
}
.auto-badge {
font-size: 0.625rem;
padding: 2px 6px;
background: var(--warning);
color: var(--bg-primary);
border-radius: var(--radius-sm);
font-weight: 600;
}
.entry-change {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-top: var(--spacing-xs);
padding: var(--spacing-xs);
background: var(--bg-primary);
border-radius: var(--radius-sm);
font-family: var(--font-mono);
font-size: 0.75rem;
}
.old-value {
color: var(--error);
text-decoration: line-through;
}
.arrow {
color: var(--text-secondary);
}
.new-value {
color: var(--success);
}
.entry-reason {
margin-top: var(--spacing-xs);
font-size: 0.75rem;
color: var(--text-secondary);
}
.actions {
display: flex;
gap: var(--spacing-sm);
margin-top: var(--spacing-md);
}
.actions button {
flex: 1;
padding: var(--spacing-sm);
border-radius: var(--radius-md);
font-size: 0.75rem;
background: var(--bg-secondary);
border: 1px solid var(--bg-tertiary);
transition: background 0.2s ease;
}
.actions button:hover {
background: var(--bg-tertiary);
}
</style>

View file

@ -0,0 +1,267 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
export let visible = false;
export let problem = '';
export let oldApproach = '';
export let newApproach = '';
export let occurrenceCount = 1;
let savePermanently = true;
let projectOnly = false;
const dispatch = createEventDispatcher();
function handleSave() {
dispatch('save', {
permanent: savePermanently,
projectOnly
});
visible = false;
}
function handleIgnore() {
dispatch('ignore');
visible = false;
}
</script>
{#if visible}
<div class="modal-backdrop" on:click={handleIgnore}>
<div class="modal" on:click|stopPropagation>
<div class="modal-header">
<span class="icon"></span>
<h2>Vorgehensweise korrigiert</h2>
</div>
<div class="modal-body">
<div class="problem-section">
<label>Problem erkannt:</label>
<p class="problem-text">{problem}</p>
</div>
<div class="correction-section">
<label>Korrektur:</label>
<div class="correction-diff">
<div class="old-approach">
<span class="label">VORHER:</span>
<code>{oldApproach}</code>
</div>
<div class="arrow"></div>
<div class="new-approach">
<span class="label">NACHHER:</span>
<code>{newApproach}</code>
</div>
</div>
</div>
<div class="occurrence-info">
Aufgetreten: <strong>{occurrenceCount}x</strong> in dieser Session
</div>
<div class="options">
<label class="checkbox-option">
<input type="checkbox" bind:checked={savePermanently} />
<span>✓ Dauerhaft speichern</span>
</label>
<label class="checkbox-option">
<input type="checkbox" bind:checked={projectOnly} disabled={!savePermanently} />
<span>Nur für dieses Projekt</span>
</label>
</div>
</div>
<div class="modal-actions">
<button class="btn-ignore" on:click={handleIgnore}>
Ignorieren
</button>
<button class="btn-save" on:click={handleSave}>
Speichern
</button>
</div>
</div>
</div>
{/if}
<style>
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background: var(--bg-primary);
border: 1px solid var(--warning);
border-radius: var(--radius-lg);
width: 90%;
max-width: 500px;
box-shadow: 0 0 30px rgba(251, 191, 36, 0.2);
}
.modal-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-md);
background: var(--bg-secondary);
border-bottom: 1px solid var(--bg-tertiary);
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.modal-header .icon {
font-size: 1.5rem;
}
.modal-header h2 {
font-size: 1rem;
font-weight: 600;
color: var(--warning);
}
.modal-body {
padding: var(--spacing-md);
}
.problem-section,
.correction-section {
margin-bottom: var(--spacing-md);
}
label {
display: block;
font-size: 0.75rem;
color: var(--text-secondary);
margin-bottom: var(--spacing-xs);
}
.problem-text {
font-size: 0.875rem;
color: var(--text-primary);
padding: var(--spacing-sm);
background: var(--bg-secondary);
border-radius: var(--radius-sm);
}
.correction-diff {
background: var(--bg-secondary);
border-radius: var(--radius-md);
overflow: hidden;
}
.old-approach,
.new-approach {
padding: var(--spacing-sm);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.old-approach {
background: rgba(239, 68, 68, 0.1);
}
.old-approach .label {
color: var(--error);
font-size: 0.625rem;
font-weight: 600;
}
.old-approach code {
color: var(--error);
text-decoration: line-through;
}
.new-approach {
background: rgba(74, 222, 128, 0.1);
}
.new-approach .label {
color: var(--success);
font-size: 0.625rem;
font-weight: 600;
}
.new-approach code {
color: var(--success);
}
.arrow {
text-align: center;
padding: var(--spacing-xs);
color: var(--text-secondary);
font-size: 0.75rem;
}
code {
font-family: var(--font-mono);
font-size: 0.75rem;
}
.occurrence-info {
font-size: 0.75rem;
color: var(--text-secondary);
margin-bottom: var(--spacing-md);
}
.options {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.checkbox-option {
display: flex;
align-items: center;
gap: var(--spacing-sm);
cursor: pointer;
}
.checkbox-option input {
width: 16px;
height: 16px;
}
.checkbox-option span {
font-size: 0.875rem;
}
.modal-actions {
display: flex;
gap: var(--spacing-sm);
padding: var(--spacing-md);
background: var(--bg-secondary);
border-top: 1px solid var(--bg-tertiary);
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
.modal-actions button {
flex: 1;
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
font-weight: 600;
transition: all 0.2s ease;
}
.btn-ignore {
background: var(--bg-tertiary);
color: var(--text-secondary);
}
.btn-ignore:hover {
background: var(--bg-primary);
}
.btn-save {
background: var(--warning);
color: var(--bg-primary);
}
.btn-save:hover {
background: #f59e0b;
}
</style>

View file

@ -0,0 +1,225 @@
<script lang="ts">
import { messages, currentInput, isProcessing, addMessage } from '$lib/stores/app';
async function sendMessage() {
const text = $currentInput.trim();
if (!text || $isProcessing) return;
// Nachricht hinzufügen
addMessage('user', text);
$currentInput = '';
$isProcessing = true;
// TODO: An Claude senden via Tauri
// Placeholder-Antwort
setTimeout(() => {
addMessage('assistant', 'Ich bin noch nicht mit dem Claude SDK verbunden. Die Integration folgt in Phase 1.3.');
$isProcessing = false;
}, 1000);
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
</script>
<div class="chat-panel">
<div class="chat-header">
<h2>💬 Chat</h2>
</div>
<div class="chat-messages">
{#if $messages.length === 0}
<div class="empty-state">
<p>Starte eine Konversation mit Claude.</p>
<p class="hint">Drücke Enter zum Senden, Shift+Enter für neue Zeile.</p>
</div>
{:else}
{#each $messages as message}
<div class="message" class:user={message.role === 'user'} class:assistant={message.role === 'assistant'}>
<div class="message-header">
<span class="message-role">
{message.role === 'user' ? '👤 Du' : '🤖 Claude'}
</span>
<span class="message-time">
{message.timestamp.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
<div class="message-content">
{message.content}
</div>
</div>
{/each}
{/if}
{#if $isProcessing}
<div class="message assistant">
<div class="message-header">
<span class="message-role">🤖 Claude</span>
</div>
<div class="message-content typing">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
</div>
{/if}
</div>
<div class="chat-input">
<textarea
bind:value={$currentInput}
on:keydown={handleKeydown}
placeholder="Nachricht eingeben..."
disabled={$isProcessing}
rows="3"
></textarea>
<button
class="send-button"
on:click={sendMessage}
disabled={!$currentInput.trim() || $isProcessing}
>
Senden
</button>
</div>
</div>
<style>
.chat-panel {
display: flex;
flex-direction: column;
height: 100%;
}
.chat-header {
padding: var(--spacing-sm) var(--spacing-md);
background: var(--bg-secondary);
border-bottom: 1px solid var(--bg-tertiary);
}
.chat-header h2 {
font-size: 0.875rem;
font-weight: 600;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: var(--spacing-md);
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-secondary);
text-align: center;
}
.empty-state .hint {
font-size: 0.75rem;
margin-top: var(--spacing-sm);
}
.message {
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
background: var(--bg-secondary);
}
.message.user {
background: var(--bg-tertiary);
margin-left: var(--spacing-xl);
}
.message.assistant {
margin-right: var(--spacing-xl);
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-xs);
}
.message-role {
font-size: 0.75rem;
font-weight: 600;
}
.message-time {
font-size: 0.625rem;
color: var(--text-secondary);
}
.message-content {
font-size: 0.875rem;
line-height: 1.5;
white-space: pre-wrap;
}
/* Typing-Animation */
.typing {
display: flex;
gap: 4px;
}
.dot {
width: 8px;
height: 8px;
background: var(--text-secondary);
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out;
}
.dot:nth-child(1) { animation-delay: -0.32s; }
.dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
/* Input-Bereich */
.chat-input {
display: flex;
gap: var(--spacing-sm);
padding: var(--spacing-md);
background: var(--bg-secondary);
border-top: 1px solid var(--bg-tertiary);
}
.chat-input textarea {
flex: 1;
resize: none;
font-size: 0.875rem;
}
.send-button {
padding: var(--spacing-sm) var(--spacing-md);
background: var(--accent);
color: white;
font-weight: 600;
border-radius: var(--radius-md);
transition: all 0.2s ease;
}
.send-button:hover:not(:disabled) {
background: var(--accent-hover);
}
.send-button:disabled {
background: var(--bg-tertiary);
color: var(--text-secondary);
cursor: not-allowed;
}
</style>

View file

@ -0,0 +1,241 @@
<script lang="ts">
import { onMount } from 'svelte';
interface MemoryStats {
total: number;
sticky: number;
by_category: Record<string, number>;
}
let stats: MemoryStats = {
total: 0,
sticky: 0,
by_category: {}
};
let lastSync = 'Noch nie';
let loading = true;
// Kategorien mit Icons
const categoryIcons: Record<string, string> = {
Critical: '🔑',
Pattern: '📋',
Preference: '⚙️',
GuardRail: '🛡️',
Hook: '🪝',
Skill: '🎯',
MCP: '🔌'
};
onMount(async () => {
// TODO: Echte Daten laden via Tauri
// const result = await invoke('load_memory');
// 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;
// TODO: Mit claude-db synchronisieren
await new Promise((r) => setTimeout(r, 1000));
lastSync = 'gerade eben';
loading = false;
}
</script>
<div class="memory-panel">
<div class="panel-header">
<h2>🧠 Gedächtnis-System</h2>
<span class="sync-status">
{#if loading}
<span class="loading">Synchronisiere...</span>
{:else}
Letzter Sync: {lastSync}
{/if}
</span>
</div>
<div class="stats-summary">
<div class="stat-item">
<span class="stat-value">{stats.total}</span>
<span class="stat-label">Einträge gesamt</span>
</div>
<div class="stat-item sticky">
<span class="stat-value">{stats.sticky}</span>
<span class="stat-label">Sticky (nie vergessen)</span>
</div>
</div>
<div class="category-list">
<h3>📥 Automatisch geladen:</h3>
{#each Object.entries(stats.by_category) as [category, count]}
<div class="category-item">
<span class="category-icon">{categoryIcons[category] || '📦'}</span>
<span class="category-name">{category}</span>
<span class="category-count">{count}</span>
</div>
{/each}
</div>
<div class="actions">
<button class="btn-sync" on:click={syncNow} disabled={loading}>
🔄 Jetzt synchronisieren
</button>
<button class="btn-settings">
⚙️ Einstellungen
</button>
</div>
</div>
<style>
.memory-panel {
padding: var(--spacing-md);
height: 100%;
display: flex;
flex-direction: column;
}
.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;
}
.sync-status {
font-size: 0.75rem;
color: var(--text-secondary);
}
.loading {
color: var(--accent);
animation: pulse 1s ease-in-out infinite;
}
.stats-summary {
display: flex;
gap: var(--spacing-md);
margin-bottom: var(--spacing-md);
}
.stat-item {
flex: 1;
padding: var(--spacing-md);
background: var(--bg-secondary);
border-radius: var(--radius-md);
text-align: center;
}
.stat-item.sticky {
border: 1px solid var(--success);
}
.stat-value {
display: block;
font-size: 1.5rem;
font-weight: 700;
color: var(--text-primary);
}
.stat-label {
font-size: 0.75rem;
color: var(--text-secondary);
}
.category-list {
flex: 1;
overflow-y: auto;
}
.category-list h3 {
font-size: 0.875rem;
margin-bottom: var(--spacing-sm);
color: var(--text-secondary);
}
.category-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-sm);
transition: background 0.2s ease;
}
.category-item:hover {
background: var(--bg-secondary);
}
.category-icon {
width: 24px;
text-align: center;
}
.category-name {
flex: 1;
font-size: 0.875rem;
}
.category-count {
font-size: 0.875rem;
font-weight: 600;
color: var(--accent);
}
.actions {
display: flex;
gap: var(--spacing-sm);
margin-top: var(--spacing-md);
}
.actions button {
flex: 1;
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s ease;
}
.btn-sync {
background: var(--accent);
color: white;
}
.btn-sync:hover:not(:disabled) {
background: var(--accent-hover);
}
.btn-sync:disabled {
background: var(--bg-tertiary);
color: var(--text-secondary);
}
.btn-settings {
background: var(--bg-secondary);
border: 1px solid var(--bg-tertiary);
}
.btn-settings:hover {
background: var(--bg-tertiary);
}
</style>

View file

@ -0,0 +1,86 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
export let disabled = false;
const dispatch = createEventDispatcher();
function handleClick() {
if (!disabled) {
dispatch('click');
}
}
</script>
<button
class="stop-button"
class:disabled
on:click={handleClick}
{disabled}
aria-label="Alle Agents sofort stoppen"
>
<span class="stop-icon"></span>
<span class="stop-text">STOPP — Alles sofort abbrechen</span>
<span class="stop-hint">(Escape)</span>
</button>
<style>
.stop-button {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-md);
padding: var(--spacing-md) var(--spacing-lg);
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 #ef4444;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 15px rgba(220, 38, 38, 0.4);
}
.stop-button:not(.disabled):hover {
background: linear-gradient(135deg, #ef4444, #dc2626);
transform: translateY(-1px);
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(220, 38, 38, 0.4);
}
.stop-button.disabled {
background: var(--bg-tertiary);
border-color: var(--bg-tertiary);
color: var(--text-secondary);
cursor: not-allowed;
box-shadow: none;
}
.stop-icon {
font-size: 1.25rem;
}
.stop-hint {
font-size: 0.75rem;
font-weight: 400;
opacity: 0.8;
}
/* Animierter Rand wenn aktiv */
.stop-button:not(.disabled) {
animation: border-pulse 1.5s ease-in-out infinite;
}
@keyframes border-pulse {
0%, 100% { border-color: #ef4444; }
50% { border-color: #fca5a5; }
}
</style>

2
src/lib/index.ts Normal file
View file

@ -0,0 +1,2 @@
// Haupt-Export für $lib
export * from './stores';

148
src/lib/stores/app.ts Normal file
View file

@ -0,0 +1,148 @@
// Claude Desktop — App-State
import { writable, derived } from 'svelte/store';
// Typen
export interface Agent {
id: string;
type: 'main' | 'explore' | 'plan' | 'bash';
status: 'active' | 'waiting' | 'idle' | 'stopped';
task: string;
startedAt: Date;
toolCalls: ToolCall[];
}
export interface ToolCall {
id: string;
agentId: string;
tool: string;
args: Record<string, unknown>;
status: 'running' | 'completed' | 'failed';
startedAt: Date;
completedAt?: Date;
result?: unknown;
}
export interface Message {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: Date;
agentId?: string;
}
export interface Permission {
id: string;
pattern: string;
type: 'session' | 'permanent';
action: 'allow' | 'deny';
createdAt: Date;
}
// Stores
export const agents = writable<Agent[]>([]);
export const toolCalls = writable<ToolCall[]>([]);
export const messages = writable<Message[]>([]);
export const permissions = writable<Permission[]>([]);
// UI-State
export const isProcessing = writable(false);
export const currentInput = writable('');
export const selectedAgentId = writable<string | null>(null);
// Abgeleitete Stores
export const activeAgents = derived(agents, ($agents) =>
$agents.filter((a) => a.status === 'active')
);
export const recentToolCalls = derived(toolCalls, ($toolCalls) =>
$toolCalls.slice(-50).reverse()
);
export const agentCount = derived(agents, ($agents) => ({
total: $agents.length,
active: $agents.filter((a) => a.status === 'active').length,
waiting: $agents.filter((a) => a.status === 'waiting').length,
idle: $agents.filter((a) => a.status === 'idle').length
}));
// Aktionen
export function addMessage(role: Message['role'], content: string, agentId?: string) {
messages.update((msgs) => [
...msgs,
{
id: crypto.randomUUID(),
role,
content,
timestamp: new Date(),
agentId
}
]);
}
export function addAgent(type: Agent['type'], task: string): string {
const id = crypto.randomUUID();
agents.update((ags) => [
...ags,
{
id,
type,
status: 'active',
task,
startedAt: new Date(),
toolCalls: []
}
]);
return id;
}
export function updateAgentStatus(id: string, status: Agent['status']) {
agents.update((ags) =>
ags.map((a) => (a.id === id ? { ...a, status } : a))
);
}
export function addToolCall(agentId: string, tool: string, args: Record<string, unknown>): string {
const id = crypto.randomUUID();
const call: ToolCall = {
id,
agentId,
tool,
args,
status: 'running',
startedAt: new Date()
};
toolCalls.update((calls) => [...calls, call]);
// Auch im Agent speichern
agents.update((ags) =>
ags.map((a) =>
a.id === agentId ? { ...a, toolCalls: [...a.toolCalls, call] } : a
)
);
return id;
}
export function completeToolCall(id: string, result: unknown, failed = false) {
toolCalls.update((calls) =>
calls.map((c) =>
c.id === id
? {
...c,
status: failed ? 'failed' : 'completed',
completedAt: new Date(),
result
}
: c
)
);
}
export function clearAll() {
agents.set([]);
toolCalls.set([]);
messages.set([]);
isProcessing.set(false);
}

2
src/lib/stores/index.ts Normal file
View file

@ -0,0 +1,2 @@
// Stores re-export
export * from './app';

159
src/routes/+layout.svelte Normal file
View file

@ -0,0 +1,159 @@
<script lang="ts">
import '../app.css';
import { isProcessing, agentCount } from '$lib/stores/app';
import StopButton from '$lib/components/StopButton.svelte';
// STOPP-Funktion
async function handleStop() {
console.log('STOPP gedrückt — breche alle Agents ab');
// TODO: Tauri-Command zum Abbrechen aufrufen
$isProcessing = false;
}
// Hotkey: Escape zum Stoppen
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape' && $isProcessing) {
handleStop();
}
}
</script>
<svelte:window on:keydown={handleKeydown} />
<div class="app-container">
<!-- Titelleiste -->
<header class="titlebar">
<div class="titlebar-left">
<span class="app-icon">🤖</span>
<h1>Claude Desktop</h1>
</div>
<div class="titlebar-center">
{#if $isProcessing}
<span class="status-indicator active"></span>
<span>Arbeitet...</span>
{:else}
<span class="status-indicator idle"></span>
<span>Bereit</span>
{/if}
</div>
<div class="titlebar-right">
<span class="agent-stats">
{$agentCount.active} aktiv | {$agentCount.waiting} wartend | {$agentCount.idle} idle
</span>
</div>
</header>
<!-- Haupt-Inhalt mit 3 Panels -->
<main class="main-content">
<slot />
</main>
<!-- STOPP-Button (immer sichtbar, unten) -->
<footer class="stop-footer" class:active={$isProcessing}>
<StopButton on:click={handleStop} disabled={!$isProcessing} />
<div class="footer-stats">
<span>Token: 0 / 200k</span>
<span>|</span>
<span>CPU: 0%</span>
</div>
</footer>
</div>
<style>
.app-container {
display: flex;
flex-direction: column;
height: 100vh;
background: var(--bg-primary);
}
/* Titelleiste */
.titlebar {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--bg-secondary);
border-bottom: 1px solid var(--bg-tertiary);
-webkit-app-region: drag;
user-select: none;
}
.titlebar-left {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.app-icon {
font-size: 1.5rem;
}
.titlebar h1 {
font-size: 1rem;
font-weight: 600;
}
.titlebar-center {
display: flex;
align-items: center;
gap: var(--spacing-xs);
font-size: 0.875rem;
color: var(--text-secondary);
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
}
.status-indicator.active {
background: var(--success);
animation: pulse 1.5s ease-in-out infinite;
}
.status-indicator.idle {
background: var(--text-secondary);
}
.titlebar-right {
font-size: 0.75rem;
color: var(--text-secondary);
}
/* Haupt-Inhalt */
.main-content {
flex: 1;
overflow: hidden;
}
/* STOPP-Footer */
.stop-footer {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-sm) var(--spacing-md);
background: var(--bg-secondary);
border-top: 1px solid var(--bg-tertiary);
transition: border-color 0.3s ease;
}
.stop-footer.active {
border-top: 2px solid var(--accent);
animation: glow 1.5s ease-in-out infinite;
}
@keyframes glow {
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 {
display: flex;
gap: var(--spacing-md);
font-size: 0.75rem;
color: var(--text-secondary);
}
</style>

96
src/routes/+page.svelte Normal file
View file

@ -0,0 +1,96 @@
<script lang="ts">
import ChatPanel from '$lib/components/ChatPanel.svelte';
import ActivityPanel from '$lib/components/ActivityPanel.svelte';
import AgentView from '$lib/components/AgentView.svelte';
</script>
<div class="panels">
<!-- Linkes Panel: Chat -->
<section class="panel panel-chat">
<ChatPanel />
</section>
<!-- Mittleres Panel: Aktivität + Agents -->
<section class="panel panel-activity">
<div class="panel-tabs">
<button class="tab active">📋 Aktivität</button>
<button class="tab">🤖 Agents</button>
</div>
<div class="panel-content">
<ActivityPanel />
</div>
</section>
<!-- Rechtes Panel: Agent-Details / Präsentation -->
<section class="panel panel-details">
<AgentView />
</section>
</div>
<style>
.panels {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1px;
height: 100%;
background: var(--bg-tertiary);
}
.panel {
display: flex;
flex-direction: column;
background: var(--bg-primary);
overflow: hidden;
}
.panel-tabs {
display: flex;
background: var(--bg-secondary);
border-bottom: 1px solid var(--bg-tertiary);
}
.tab {
flex: 1;
padding: var(--spacing-sm) var(--spacing-md);
font-size: 0.875rem;
color: var(--text-secondary);
background: transparent;
border: none;
cursor: pointer;
transition: all 0.2s ease;
}
.tab:hover {
color: var(--text-primary);
background: var(--bg-tertiary);
}
.tab.active {
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>

20
svelte.config.js Normal file
View file

@ -0,0 +1,20 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
// SPA-Modus für Tauri (wichtig!)
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: 'index.html',
precompress: false,
strict: true
})
}
};
export default config;

937
tools.yaml Normal file
View file

@ -0,0 +1,937 @@
---
version: v1.2
tools:
## Access Groups
## An access group is the equivalent of an Endpoint Group in Portainer.
## ------------------------------------------------------------
- name: listAccessGroups
description: List all available access groups
annotations:
title: List Access Groups
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: createAccessGroup
description: Create a new access group. Use access groups when you want to define
accesses on more than one environment. Otherwise, define the accesses on
the environment level.
parameters:
- name: name
description: The name of the access group
type: string
required: true
- name: environmentIds
description: "The IDs of the environments that are part of the access group.
Must include all the environment IDs that are part of the group - this
includes new environments and the existing environments that are
already associated with the group. Example: [1, 2, 3]"
type: array
items:
type: number
annotations:
title: Create Access Group
readOnlyHint: false
destructiveHint: false
idempotentHint: false
openWorldHint: false
- name: updateAccessGroupName
description: Update the name of an existing access group.
parameters:
- name: id
description: The ID of the access group to update
type: number
required: true
- name: name
description: The name of the access group
type: string
required: true
annotations:
title: Update Access Group Name
readOnlyHint: false
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: updateAccessGroupUserAccesses
description: Update the user accesses of an existing access group.
parameters:
- name: id
description: The ID of the access group to update
type: number
required: true
- name: userAccesses
description: "The user accesses that are associated with all the environments in
the access group. The ID is the user ID of the user in Portainer.
Example: [{id: 1, access: 'environment_administrator'}, {id: 2,
access: 'standard_user'}]"
type: array
required: true
items:
type: object
properties:
id:
description: The ID of the user
type: number
access:
description: The access level of the user. Can be environment_administrator,
helpdesk_user, standard_user, readonly_user or operator_user
type: string
enum:
- environment_administrator
- helpdesk_user
- standard_user
- readonly_user
- operator_user
annotations:
title: Update Access Group User Accesses
readOnlyHint: false
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: updateAccessGroupTeamAccesses
description: Update the team accesses of an existing access group.
parameters:
- name: id
description: The ID of the access group to update
type: number
required: true
- name: teamAccesses
description: "The team accesses that are associated with all the environments in
the access group. The ID is the team ID of the team in Portainer.
Example: [{id: 1, access: 'environment_administrator'}, {id: 2,
access: 'standard_user'}]"
type: array
required: true
items:
type: object
properties:
id:
description: The ID of the team
type: number
access:
description: The access level of the team. Can be environment_administrator,
helpdesk_user, standard_user, readonly_user or operator_user
type: string
enum:
- environment_administrator
- helpdesk_user
- standard_user
- readonly_user
- operator_user
annotations:
title: Update Access Group Team Accesses
readOnlyHint: false
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: addEnvironmentToAccessGroup
description: Add an environment to an access group.
parameters:
- name: id
description: The ID of the access group to update
type: number
required: true
- name: environmentId
description: The ID of the environment to add to the access group
type: number
required: true
annotations:
title: Add Environment To Access Group
readOnlyHint: false
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: removeEnvironmentFromAccessGroup
description: Remove an environment from an access group.
parameters:
- name: id
description: The ID of the access group to update
type: number
required: true
- name: environmentId
description: The ID of the environment to remove from the access group
type: number
required: true
annotations:
title: Remove Environment From Access Group
readOnlyHint: false
destructiveHint: true
idempotentHint: true
openWorldHint: false
## Environment
## ------------------------------------------------------------
- name: listEnvironments
description: List all available environments
annotations:
title: List Environments
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: updateEnvironmentTags
description: Update the tags associated with an environment
parameters:
- name: id
description: The ID of the environment to update
type: number
required: true
- name: tagIds
description: >-
The IDs of the tags that are associated with the environment.
Must include all the tag IDs that should be associated with the environment - this includes new tags and existing tags.
Providing an empty array will remove all tags.
Example: [1, 2, 3]
type: array
required: true
items:
type: number
annotations:
title: Update Environment Tags
readOnlyHint: false
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: updateEnvironmentUserAccesses
description: Update the user access policies of an environment
parameters:
- name: id
description: The ID of the environment to update
type: number
required: true
- name: userAccesses
description: >-
The user accesses that are associated with the environment.
The ID is the user ID of the user in Portainer.
Must include all the access policies for all users that should be associated with the environment.
Providing an empty array will remove all user accesses.
Example: [{id: 1, access: 'environment_administrator'}, {id: 2, access: 'standard_user'}]
type: array
required: true
items:
type: object
properties:
id:
description: The ID of the user
type: number
access:
description: The access level of the user
type: string
enum:
- environment_administrator
- helpdesk_user
- standard_user
- readonly_user
- operator_user
annotations:
title: Update Environment User Accesses
readOnlyHint: false
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: updateEnvironmentTeamAccesses
description: Update the team access policies of an environment
parameters:
- name: id
description: The ID of the environment to update
type: number
required: true
- name: teamAccesses
description: >-
The team accesses that are associated with the environment.
The ID is the team ID of the team in Portainer.
Must include all the access policies for all teams that should be associated with the environment.
Providing an empty array will remove all team accesses.
Example: [{id: 1, access: 'environment_administrator'}, {id: 2, access: 'standard_user'}]
type: array
required: true
items:
type: object
properties:
id:
description: The ID of the team
type: number
access:
description: The access level of the team
type: string
enum:
- environment_administrator
- helpdesk_user
- standard_user
- readonly_user
- operator_user
annotations:
title: Update Environment Team Accesses
readOnlyHint: false
destructiveHint: false
idempotentHint: true
openWorldHint: false
## Environment Groups
## An environment group is the equivalent of an Edge Group in Portainer.
## ------------------------------------------------------------
- name: createEnvironmentGroup
description: Create a new environment group. Environment groups are the equivalent of Edge Groups in Portainer.
parameters:
- name: name
description: The name of the environment group
type: string
required: true
- name: environmentIds
description: The IDs of the environments to add to the group
type: array
required: true
items:
type: number
annotations:
title: Create Environment Group
readOnlyHint: false
destructiveHint: false
idempotentHint: false
openWorldHint: false
- name: listEnvironmentGroups
description: List all available environment groups. Environment groups are the equivalent of Edge Groups in Portainer.
annotations:
title: List Environment Groups
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: updateEnvironmentGroupName
description: Update the name of an environment group. Environment groups are the equivalent of Edge Groups in Portainer.
parameters:
- name: id
description: The ID of the environment group to update
type: number
required: true
- name: name
description: The new name for the environment group
type: string
required: true
annotations:
title: Update Environment Group Name
readOnlyHint: false
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: updateEnvironmentGroupEnvironments
description: Update the environments associated with an environment group. Environment groups are the equivalent of Edge Groups in Portainer.
parameters:
- name: id
description: The ID of the environment group to update
type: number
required: true
- name: environmentIds
description: >-
The IDs of the environments that should be part of the group.
Must include all environment IDs that should be associated with the group.
Providing an empty array will remove all environments from the group.
Example: [1, 2, 3]
type: array
required: true
items:
type: number
annotations:
title: Update Environment Group Environments
readOnlyHint: false
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: updateEnvironmentGroupTags
description: Update the tags associated with an environment group. Environment groups are the equivalent of Edge Groups in Portainer.
parameters:
- name: id
description: The ID of the environment group to update
type: number
required: true
- name: tagIds
description: >-
The IDs of the tags that should be associated with the group.
Must include all tag IDs that should be associated with the group.
Providing an empty array will remove all tags from the group.
Example: [1, 2, 3]
type: array
required: true
items:
type: number
annotations:
title: Update Environment Group Tags
readOnlyHint: false
destructiveHint: false
idempotentHint: true
openWorldHint: false
## Settings
## ------------------------------------------------------------
- name: getSettings
description: Get the settings of the Portainer instance
annotations:
title: Get Settings
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: false
## Stacks
## ------------------------------------------------------------
- name: listStacks
description: List all available stacks
annotations:
title: List Stacks
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: getStackFile
description: Get the compose file for a specific stack ID
parameters:
- name: id
description: The ID of the stack to get the compose file for
type: number
required: true
annotations:
title: Get Stack File
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: createStack
description: Create a new stack
parameters:
- name: name
description: Name of the stack. Stack name must only consist of lowercase alpha
characters, numbers, hyphens, or underscores as well as start with a
lowercase character or number
type: string
required: true
- name: file
description: >-
Content of the stack file. The file must be a valid
docker-compose.yml file. example: services:
web:
image:nginx
type: string
required: true
- name: environmentGroupIds
description: "The IDs of the environment groups that the stack belongs to. Must
include at least one environment group ID. Example: [1, 2, 3]"
type: array
required: true
items:
type: number
annotations:
title: Create Stack
readOnlyHint: false
destructiveHint: false
idempotentHint: false
openWorldHint: false
- name: updateStack
description: Update an existing stack
parameters:
- name: id
description: The ID of the stack to update
type: number
required: true
- name: file
description: >-
Content of the stack file. The file must be a valid
docker-compose.yml file. example: version: 3
services:
web:
image:nginx
type: string
required: true
- name: environmentGroupIds
description: "The IDs of the environment groups that the stack belongs to. Must
include at least one environment group ID. Example: [1, 2, 3]"
type: array
required: true
items:
type: number
annotations:
title: Update Stack
readOnlyHint: false
destructiveHint: false
idempotentHint: true
openWorldHint: false
## Local Stacks (regular Docker Compose stacks, non-Edge)
## ------------------------------------------------------------
- name: listLocalStacks
description: >-
List all local (non-edge) stacks deployed on Portainer environments.
Returns stack ID, name, status, type, environment ID, creation date,
and environment variables for each stack.
annotations:
title: List Local Stacks
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: getLocalStackFile
description: >-
Get the docker-compose file content for a specific local stack by its ID.
Returns the raw compose file as text.
parameters:
- name: id
description: The ID of the local stack to get the compose file for
type: number
required: true
annotations:
title: Get Local Stack File
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: createLocalStack
description: >-
Create a new local standalone Docker Compose stack on a specific environment.
Requires the environment ID, a stack name, and the compose file content.
Optionally accepts environment variables.
parameters:
- name: environmentId
description: The ID of the environment to deploy the stack to
type: number
required: true
- name: name
description: >-
Name of the stack. Stack name must only consist of lowercase alpha
characters, numbers, hyphens, or underscores as well as start with a
lowercase character or number
type: string
required: true
- name: file
description: >-
Content of the stack file. The file must be a valid
docker-compose.yml file. example: services:
web:
image: nginx
type: string
required: true
- name: env
description: >-
Optional environment variables for the stack. Each variable must have
a 'name' and 'value' field.
Example: [{"name": "DB_HOST", "value": "localhost"}]
type: array
required: false
items:
type: object
properties:
name:
type: string
description: The name of the environment variable
value:
type: string
description: The value of the environment variable
annotations:
title: Create Local Stack
readOnlyHint: false
destructiveHint: false
idempotentHint: false
openWorldHint: false
- name: updateLocalStack
description: >-
Update an existing local stack with new compose file content and/or
environment variables. Requires the stack ID and environment ID.
parameters:
- name: id
description: The ID of the local stack to update
type: number
required: true
- name: environmentId
description: The ID of the environment where the stack is deployed
type: number
required: true
- name: file
description: >-
Content of the stack file. The file must be a valid
docker-compose.yml file. example: services:
web:
image: nginx
type: string
required: true
- name: env
description: >-
Optional environment variables for the stack. Each variable must have
a 'name' and 'value' field.
Example: [{"name": "DB_HOST", "value": "localhost"}]
type: array
required: false
items:
type: object
properties:
name:
type: string
description: The name of the environment variable
value:
type: string
description: The value of the environment variable
- name: prune
description: >-
If true, services that are no longer in the compose file will be removed.
Default: false
type: boolean
required: false
- name: pullImage
description: >-
If true, images will be pulled before deploying. Default: false
type: boolean
required: false
annotations:
title: Update Local Stack
readOnlyHint: false
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: startLocalStack
description: >-
Start a stopped local stack. Brings up all containers defined in the
stack's compose file.
parameters:
- name: id
description: The ID of the local stack to start
type: number
required: true
- name: environmentId
description: The ID of the environment where the stack is deployed
type: number
required: true
annotations:
title: Start Local Stack
readOnlyHint: false
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: stopLocalStack
description: >-
Stop a running local stack. Stops all containers defined in the
stack's compose file.
parameters:
- name: id
description: The ID of the local stack to stop
type: number
required: true
- name: environmentId
description: The ID of the environment where the stack is deployed
type: number
required: true
annotations:
title: Stop Local Stack
readOnlyHint: false
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: deleteLocalStack
description: >-
Delete a local stack permanently. This removes the stack and all its
associated containers from the environment.
parameters:
- name: id
description: The ID of the local stack to delete
type: number
required: true
- name: environmentId
description: The ID of the environment where the stack is deployed
type: number
required: true
annotations:
title: Delete Local Stack
readOnlyHint: false
destructiveHint: true
idempotentHint: false
openWorldHint: false
## Tags
## ------------------------------------------------------------
- name: createEnvironmentTag
description: Create a new environment tag
parameters:
- name: name
description: The name of the tag
type: string
required: true
annotations:
title: Create Environment Tag
readOnlyHint: false
destructiveHint: false
idempotentHint: false
openWorldHint: false
- name: listEnvironmentTags
description: List all available environment tags
annotations:
title: List Environment Tags
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: false
## Teams
## ------------------------------------------------------------
- name: createTeam
description: Create a new team
parameters:
- name: name
description: The name of the team
type: string
required: true
annotations:
title: Create Team
readOnlyHint: false
destructiveHint: false
idempotentHint: false
openWorldHint: false
- name: listTeams
description: List all available teams
annotations:
title: List Teams
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: updateTeamName
description: Update the name of an existing team
parameters:
- name: id
description: The ID of the team to update
type: number
required: true
- name: name
description: The new name of the team
type: string
required: true
annotations:
title: Update Team Name
readOnlyHint: false
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: updateTeamMembers
description: Update the members of an existing team
parameters:
- name: id
description: The ID of the team to update
type: number
required: true
- name: userIds
description: "The IDs of the users that are part of the team. Must include all
the user IDs that are part of the team - this includes new users and
the existing users that are already associated with the team. Example:
[1, 2, 3]"
type: array
required: true
items:
type: number
annotations:
title: Update Team Members
readOnlyHint: false
destructiveHint: false
idempotentHint: true
openWorldHint: false
## Users
## ------------------------------------------------------------
- name: listUsers
description: List all available users
annotations:
title: List Users
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: false
- name: updateUserRole
description: Update an existing user
parameters:
- name: id
description: The ID of the user to update
type: number
required: true
- name: role
description: The role of the user. Can be admin, user or edge_admin
type: string
required: true
enum:
- admin
- user
- edge_admin
annotations:
title: Update User Role
readOnlyHint: false
destructiveHint: false
idempotentHint: true
openWorldHint: false
## Docker Proxy
## ------------------------------------------------------------
- name: dockerProxy
description: Proxy Docker requests to a specific Portainer environment.
This tool can be used with any Docker API operation as documented in the Docker Engine API specification (https://docs.docker.com/reference/api/engine/version/v1.48/).
In read-only mode, only GET requests are allowed.
parameters:
- name: environmentId
description: The ID of the environment to proxy Docker requests to
type: number
required: true
- name: method
description: The HTTP method to use to proxy the Docker API operation
type: string
required: true
enum:
- GET
- POST
- PUT
- DELETE
- HEAD
- name: dockerAPIPath
description: "The route of the Docker API operation to proxy. Must include the leading slash. Example: /containers/json"
type: string
required: true
- name: queryParams
description: "The query parameters to include in the Docker API operation. Must be an array of key-value pairs.
Example: [{key: 'all', value: 'true'}, {key: 'filter', value: 'dangling'}]"
type: array
required: false
items:
type: object
properties:
key:
type: string
description: The key of the query parameter
value:
type: string
description: The value of the query parameter
- name: headers
description: "The headers to include in the Docker API operation. Must be an array of key-value pairs.
Example: [{key: 'Content-Type', value: 'application/json'}]"
type: array
required: false
items:
type: object
properties:
key:
type: string
description: The key of the header
value:
type: string
description: The value of the header
- name: body
description: "The body of the Docker API operation to proxy. Must be a JSON string.
Example: {'Image': 'nginx:latest', 'Name': 'my-container'}"
type: string
required: false
annotations:
title: Docker Proxy
readOnlyHint: true
destructiveHint: true
idempotentHint: true
openWorldHint: false
## Kubernetes Proxy
## ------------------------------------------------------------
- name: kubernetesProxy
description: Proxy Kubernetes requests to a specific Portainer environment.
This tool can be used with any Kubernetes API operation as documented in the Kubernetes API specification (https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/).
In read-only mode, only GET requests are allowed.
parameters:
- name: environmentId
description: The ID of the environment to proxy Kubernetes requests to
type: number
required: true
- name: method
description: The HTTP method to use to proxy the Kubernetes API operation
type: string
required: true
enum:
- GET
- POST
- PUT
- DELETE
- HEAD
- name: kubernetesAPIPath
description: "The route of the Kubernetes API operation to proxy. Must include the leading slash. Example: /api/v1/namespaces/default/pods"
type: string
required: true
- name: queryParams
description: "The query parameters to include in the Kubernetes API operation. Must be an array of key-value pairs.
Example: [{key: 'watch', value: 'true'}, {key: 'fieldSelector', value: 'metadata.name=my-pod'}]"
type: array
required: false
items:
type: object
properties:
key:
type: string
description: The key of the query parameter
value:
type: string
description: The value of the query parameter
- name: headers
description: "The headers to include in the Kubernetes API operation. Must be an array of key-value pairs.
Example: [{key: 'Content-Type', value: 'application/json'}]"
type: array
required: false
items:
type: object
properties:
key:
type: string
description: The key of the header
value:
type: string
description: The value of the header
- name: body
description: "The body of the Kubernetes API operation to proxy. Must be a JSON string.
Example: {'apiVersion': 'v1', 'kind': 'Pod', 'metadata': {'name': 'my-pod'}}"
type: string
required: false
annotations:
title: Kubernetes Proxy
readOnlyHint: true
destructiveHint: true
idempotentHint: true
openWorldHint: false
- name: getKubernetesResourceStripped
description: >-
Proxy GET requests to a specific Portainer environment for Kubernetes resources,
and automatically strips verbose metadata fields (such as 'managedFields') from the API response
to reduce its size. This tool is intended for retrieving Kubernetes resource
information where a leaner payload is desired.
This tool can be used with any GET Kubernetes API operation as documented
in the Kubernetes API specification (https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/).
For other methods (POST, PUT, DELETE, HEAD), use the 'kubernetesProxy' tool.
parameters:
- name: environmentId
description: The ID of the environment to proxy Kubernetes GET requests to
type: number
required: true
- name: kubernetesAPIPath
description: "The route of the Kubernetes API GET operation to proxy. Must include the leading slash. Example: /api/v1/namespaces/default/pods"
type: string
required: true
- name: queryParams
description: "The query parameters to include in the Kubernetes API operation. Must be an array of key-value pairs.
Example: [{key: 'watch', value: 'true'}, {key: 'fieldSelector', value: 'metadata.name=my-pod'}]"
type: array
required: false
items:
type: object
properties:
key:
type: string
description: The key of the query parameter
value:
type: string
description: The value of the query parameter
- name: headers
description: "The headers to include in the Kubernetes API operation. Must be an array of key-value pairs.
Example: [{key: 'Accept', value: 'application/json'}]"
type: array
required: false
items:
type: object
properties:
key:
type: string
description: The key of the header
value:
type: string
description: The value of the header
annotations:
title: Get Kubernetes Resource (Stripped)
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: false

14
tsconfig.json Normal file
View file

@ -0,0 +1,14 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
}

26
vite.config.ts Normal file
View file

@ -0,0 +1,26 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
// Tauri erwartet einen festen Port
server: {
port: 1420,
strictPort: true,
},
// Verhindert Probleme mit Tauri
clearScreen: false,
// Umgebungsvariablen für Tauri
envPrefix: ['VITE_', 'TAURI_'],
build: {
// Tauri unterstützt es2021
target: ['es2021', 'chrome100', 'safari13'],
// Debug-Infos in Dev
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
sourcemap: !!process.env.TAURI_DEBUG,
},
});