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:
commit
2822796c7a
32 changed files with 9297 additions and 0 deletions
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal 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
352
README.md
Normal 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
29
package.json
Normal 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
66
shell.nix
Normal 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
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
29
src-tauri/Cargo.toml
Normal 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
3
src-tauri/build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
BIN
src-tauri/icons/icon.png
Normal file
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
179
src-tauri/src/audit.rs
Normal 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
50
src-tauri/src/claude.rs
Normal 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
45
src-tauri/src/lib.rs
Normal 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
6
src-tauri/src/main.rs
Normal 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
164
src-tauri/src/memory.rs
Normal 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
52
src-tauri/tauri.conf.json
Normal 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
139
src/app.css
Normal 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
13
src/app.html
Normal 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>
|
||||||
166
src/lib/components/ActivityPanel.svelte
Normal file
166
src/lib/components/ActivityPanel.svelte
Normal 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>
|
||||||
267
src/lib/components/AgentView.svelte
Normal file
267
src/lib/components/AgentView.svelte
Normal 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>
|
||||||
299
src/lib/components/AuditLog.svelte
Normal file
299
src/lib/components/AuditLog.svelte
Normal 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>
|
||||||
267
src/lib/components/AutoCorrectionModal.svelte
Normal file
267
src/lib/components/AutoCorrectionModal.svelte
Normal 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>
|
||||||
225
src/lib/components/ChatPanel.svelte
Normal file
225
src/lib/components/ChatPanel.svelte
Normal 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>
|
||||||
241
src/lib/components/MemoryPanel.svelte
Normal file
241
src/lib/components/MemoryPanel.svelte
Normal 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>
|
||||||
86
src/lib/components/StopButton.svelte
Normal file
86
src/lib/components/StopButton.svelte
Normal 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
2
src/lib/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
// Haupt-Export für $lib
|
||||||
|
export * from './stores';
|
||||||
148
src/lib/stores/app.ts
Normal file
148
src/lib/stores/app.ts
Normal 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
2
src/lib/stores/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
// Stores re-export
|
||||||
|
export * from './app';
|
||||||
159
src/routes/+layout.svelte
Normal file
159
src/routes/+layout.svelte
Normal 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
96
src/routes/+page.svelte
Normal 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
20
svelte.config.js
Normal 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
937
tools.yaml
Normal 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
14
tsconfig.json
Normal 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
26
vite.config.ts
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue