Compare commits
No commits in common. "9c495fa6d2d9e7ff5e674b792b8796320e18cdc7" and "bb3050cc1254e6bc6b347df814c210d288714933" have entirely different histories.
9c495fa6d2
...
bb3050cc12
5 changed files with 93 additions and 1216 deletions
134
ROADMAP.md
134
ROADMAP.md
|
|
@ -32,7 +32,6 @@ Stand: 14.04.2026
|
||||||
| **UI: Code-Copy, Edit, Regenerate (Phase 7)** | ✅ | 9d837ef |
|
| **UI: Code-Copy, Edit, Regenerate (Phase 7)** | ✅ | 9d837ef |
|
||||||
| **Session-Management (Phase 6)** | ✅ | abaf4eb |
|
| **Session-Management (Phase 6)** | ✅ | abaf4eb |
|
||||||
| **Claude-DB Integration (Phase 8)** | ✅ | e6bd0de |
|
| **Claude-DB Integration (Phase 8)** | ✅ | e6bd0de |
|
||||||
| **Context-Management (Phase 9)** | ✅ | eb91e54 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -185,9 +184,7 @@ Die App hat keinen direkten Zugriff auf die zentrale Wissensbasis (`claude` DB a
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 9: Intelligentes Context-Management ✅ ERLEDIGT
|
## Phase 9: Intelligentes Context-Management (WICHTIG)
|
||||||
|
|
||||||
> **Commit:** eb91e54 (14.04.2026)
|
|
||||||
|
|
||||||
### Das Problem: Context-Verlust nach Compacting
|
### Das Problem: Context-Verlust nach Compacting
|
||||||
|
|
||||||
|
|
@ -220,52 +217,108 @@ Compacting ist **notwendig** (Token-Limit, Kosten, Latenz), aber dabei geht krit
|
||||||
└─────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### Implementiert
|
### Aufgaben
|
||||||
|
|
||||||
- ✅ **src-tauri/src/context.rs** (NEU)
|
- [ ] **src-tauri/src/context.rs** (NEU)
|
||||||
- `StickyContext` Struct (Schicht 1) — User-Info, Credentials, Projekt, Regeln
|
- [ ] `StickyContext` Struct (Schicht 1)
|
||||||
- `ProjectContext` Struct (Schicht 2) — Entscheidungen, TODOs, Insights
|
- [ ] `ProjectContext` Struct (Schicht 2)
|
||||||
- `ExtractedContext` — Kontext vor Compacting extrahieren
|
- [ ] `get_sticky_context()` → ~200 Token
|
||||||
- `render()` Methoden für System-Prompt-Integration
|
- [ ] `get_project_context(project_id)` → ~500 Token
|
||||||
- Token-Schätzung mit `estimate_tokens()`
|
- [ ] `extract_critical_before_compacting()` → JSON
|
||||||
|
|
||||||
- ✅ **Datenbank-Schema** (SQLite)
|
- [ ] **src-tauri/src/credentials.rs** (NEU)
|
||||||
- `sticky_context` Tabelle (key, value, priority)
|
- [ ] Credentials aus DB laden (verschlüsselt)
|
||||||
- `compacting_archive` Tabelle (Entscheidungen, TODOs, etc.)
|
- [ ] `inject_for_context(tool_name)` → Zugang wenn nötig
|
||||||
- `context_failures` Tabelle (für Prompt-Optimierung)
|
- [ ] Credentials NIE im Chat-Verlauf speichern
|
||||||
|
|
||||||
- ✅ **Tauri-Commands**
|
- [ ] **scripts/claude-bridge.js**
|
||||||
- `get_sticky_context()` — Schicht 1 laden
|
- [ ] Schicht 1 bei jedem `query()` Call als System-Prompt
|
||||||
- `set_sticky_context()` — Eintrag setzen
|
- [ ] Hook vor Compacting: `extract_critical_before_compacting()`
|
||||||
- `remove_sticky_context()` — Eintrag löschen
|
- [ ] Hook nach Compacting: `restore_critical_context()`
|
||||||
- `get_project_context()` — Schicht 2 aus Archiv
|
|
||||||
- `extract_context_before_compacting()` — Kritisches extrahieren
|
|
||||||
- `log_context_failure()` — Fehler loggen
|
|
||||||
- `get_full_context()` — Kombinierter Prompt
|
|
||||||
- `list_sticky_context()` — Alle Einträge auflisten
|
|
||||||
|
|
||||||
- ✅ **src/lib/components/ContextPanel.svelte** (NEU)
|
- [ ] **Datenbank-Schema** (SQLite lokal)
|
||||||
- Sticky-Context-Einträge anzeigen/bearbeiten
|
```sql
|
||||||
- Eintrags-Typen: User-Info, Regeln, Credentials, Projekt
|
CREATE TABLE sticky_context (
|
||||||
- Prioritäts-Management (1=kritisch bis 4=niedrig)
|
key TEXT PRIMARY KEY,
|
||||||
- Vorschau des gerenderten Context
|
value TEXT,
|
||||||
- Token-Anzeige
|
priority INT -- 1=kritisch, niemals entfernen
|
||||||
|
);
|
||||||
|
|
||||||
- ✅ **src/routes/+page.svelte**
|
CREATE TABLE compacting_archive (
|
||||||
- Neuer Tab "📌 Context" im rechten Panel
|
id INTEGER PRIMARY KEY,
|
||||||
|
session_id TEXT,
|
||||||
|
extracted_at TEXT,
|
||||||
|
decisions JSON,
|
||||||
|
open_questions JSON,
|
||||||
|
key_insights JSON
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
### Noch offen (niedrigere Priorität)
|
- [ ] **Pre-Tool-Hook: Wissens-Hints**
|
||||||
|
- [ ] Bei Tool-Aufruf: Schlüsselwörter extrahieren
|
||||||
|
- [ ] Claude-DB nach relevantem Wissen durchsuchen
|
||||||
|
- [ ] Als `<knowledge-hint>` injizieren (max 200 Token)
|
||||||
|
|
||||||
- [ ] **Bridge-Integration** — Context bei jedem API-Call injizieren
|
### Wichtig: Nicht ALLES wiederherstellen!
|
||||||
- [ ] **Auto-Extraction vor Compacting** — Hook automatisch auslösen
|
|
||||||
- [ ] **Validation** — Prüfen ob Claude den Context nutzt
|
```
|
||||||
- [ ] **Wissens-Hints** — On-demand aus claude-db laden
|
❌ FALSCH: 130.000 Token zurück injizieren
|
||||||
|
→ Sofort wieder Compacting → Endlosschleife
|
||||||
|
|
||||||
|
✅ RICHTIG: 700 Token kritischen Kontext
|
||||||
|
Schicht 1: 200 Token (Zugänge, User)
|
||||||
|
Schicht 2: 500 Token (Projekt, Entscheidungen)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enforcement: Sicherstellen dass Claude den Kontext NUTZT
|
||||||
|
|
||||||
|
**Problem:** Injizierter Kontext kann ignoriert werden (Lost in the Middle, keine Anweisung)
|
||||||
|
|
||||||
|
**Lösung 1: Position**
|
||||||
|
- Schicht 1 → System Prompt (höchste Priorität)
|
||||||
|
- Schicht 2 → Letzter System-Reminder vor User-Nachricht (Recency Bias)
|
||||||
|
|
||||||
|
**Lösung 2: Explizite Anweisungen**
|
||||||
|
```
|
||||||
|
<critical-context>
|
||||||
|
Du MUSST folgende Zugänge verwenden (NICHT nachfragen!):
|
||||||
|
- DB: 192.168.155.11 / dolibarr_test
|
||||||
|
Diese Daten sind AKTUELL und KORREKT.
|
||||||
|
</critical-context>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Lösung 3: Validierung nach Antwort**
|
||||||
|
- [ ] `validateResponse()` in claude-bridge.js
|
||||||
|
- [ ] Prüft: Hat Claude nach Infos gefragt die injiziert waren?
|
||||||
|
- [ ] Wenn ja: Automatisch Retry mit Korrektur-Hinweis
|
||||||
|
|
||||||
|
**Lösung 4: Feedback-Loop**
|
||||||
|
- [ ] `context_failures` Tabelle in SQLite
|
||||||
|
- [ ] Speichert wenn Kontext ignoriert wurde
|
||||||
|
- [ ] Pattern erkennen → Prompts verbessern
|
||||||
|
|
||||||
|
### Aufgaben (Enforcement)
|
||||||
|
|
||||||
|
- [ ] **scripts/claude-bridge.js**
|
||||||
|
- [ ] System Prompt Builder mit `<critical-context>` Tags
|
||||||
|
- [ ] Schicht 1 am Anfang, Schicht 2 am Ende
|
||||||
|
- [ ] `validateResponse()` nach jeder Antwort
|
||||||
|
- [ ] Auto-Retry bei Kontext-Ignorierung (max 1x)
|
||||||
|
|
||||||
|
- [ ] **src-tauri/src/db.rs**
|
||||||
|
- [ ] `context_failures` Tabelle
|
||||||
|
- [ ] `log_context_failure(session_id, context, expected, actual)`
|
||||||
|
- [ ] `get_failure_patterns()` für Prompt-Optimierung
|
||||||
|
|
||||||
|
- [ ] **UI: Warnung bei Regel-Verletzung**
|
||||||
|
- [ ] Toast/Banner wenn Claude Regel ignoriert
|
||||||
|
- [ ] Option: "Erneut versuchen mit Hinweis"
|
||||||
|
|
||||||
### Verifikation
|
### Verifikation
|
||||||
```bash
|
```bash
|
||||||
# Context-Panel öffnen → Einträge hinzufügen
|
# Lange Session (>100 Nachrichten) → Compacting passiert
|
||||||
# Vorschau → <critical-context> Tags sichtbar
|
# Danach: Zugänge noch bekannt? Projekt-Kontext da?
|
||||||
# Token-Anzeige zeigt ~200 Token
|
# Tool aufrufen → Relevante Hints erscheinen?
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -1195,4 +1248,3 @@ CARGO_TARGET_DIR=/tmp/claude-desktop-target nix-shell --run "npx tauri build"
|
||||||
| 14.04.2026 | 9d837ef | **Phase 7:** UI Code-Copy, Edit, Regenerate |
|
| 14.04.2026 | 9d837ef | **Phase 7:** UI Code-Copy, Edit, Regenerate |
|
||||||
| 14.04.2026 | abaf4eb | **Phase 6:** Session-Management, Auto-Load, Compacting |
|
| 14.04.2026 | abaf4eb | **Phase 6:** Session-Management, Auto-Load, Compacting |
|
||||||
| 14.04.2026 | e6bd0de | **Phase 8:** Claude-DB Integration, KnowledgePanel |
|
| 14.04.2026 | e6bd0de | **Phase 8:** Claude-DB Integration, KnowledgePanel |
|
||||||
| 14.04.2026 | eb91e54 | **Phase 9:** Context-Management, ContextPanel |
|
|
||||||
|
|
|
||||||
|
|
@ -1,591 +0,0 @@
|
||||||
// Claude Desktop — Intelligentes Context-Management
|
|
||||||
// Drei-Schichten-Gedächtnis für kritischen Kontext
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use tauri::{AppHandle, Manager};
|
|
||||||
|
|
||||||
use crate::db::{Database, DbState};
|
|
||||||
|
|
||||||
/// Schicht 1: Immer präsent (~200 Token)
|
|
||||||
/// Wird bei JEDEM API-Call als System-Prompt gesendet
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
||||||
pub struct StickyContext {
|
|
||||||
/// User-Infos (Name, Firma)
|
|
||||||
pub user_info: Option<String>,
|
|
||||||
/// Komprimierte Zugänge (ohne Passwörter!)
|
|
||||||
pub active_credentials: Vec<CredentialHint>,
|
|
||||||
/// Aktuelles Projekt
|
|
||||||
pub current_project: Option<ProjectInfo>,
|
|
||||||
/// Kritische Regeln
|
|
||||||
pub critical_rules: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Zugangs-Hint (ohne sensible Daten!)
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct CredentialHint {
|
|
||||||
pub name: String,
|
|
||||||
pub host: String,
|
|
||||||
pub db_or_path: Option<String>,
|
|
||||||
/// Wann automatisch injizieren (Regex-Pattern)
|
|
||||||
pub inject_pattern: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Projekt-Info (kompakt)
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ProjectInfo {
|
|
||||||
pub id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub current_phase: Option<String>,
|
|
||||||
pub working_dir: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Schicht 2: Projekt-Kontext (~500 Token)
|
|
||||||
/// Wird nach Compacting neu injiziert
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
||||||
pub struct ProjectContext {
|
|
||||||
/// Komprimiertes CLAUDE.md
|
|
||||||
pub claude_md_summary: Option<String>,
|
|
||||||
/// Architektur-Entscheidungen
|
|
||||||
pub decisions: Vec<Decision>,
|
|
||||||
/// Offene TODOs
|
|
||||||
pub open_todos: Vec<String>,
|
|
||||||
/// Wichtige Erkenntnisse
|
|
||||||
pub key_insights: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Eine Architektur-Entscheidung
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Decision {
|
|
||||||
pub topic: String,
|
|
||||||
pub decision: String,
|
|
||||||
pub reason: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extrahierter Kontext vor Compacting
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ExtractedContext {
|
|
||||||
pub session_id: String,
|
|
||||||
pub extracted_at: String,
|
|
||||||
pub decisions: Vec<Decision>,
|
|
||||||
pub open_questions: Vec<String>,
|
|
||||||
pub key_insights: Vec<String>,
|
|
||||||
pub mentioned_files: Vec<String>,
|
|
||||||
pub mentioned_tools: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wissens-Hint (Schicht 3, on-demand)
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct KnowledgeHint {
|
|
||||||
pub title: String,
|
|
||||||
pub content: String,
|
|
||||||
pub relevance: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StickyContext {
|
|
||||||
/// Rendert Schicht 1 als System-Prompt-Teil
|
|
||||||
pub fn render(&self) -> String {
|
|
||||||
let mut parts = Vec::new();
|
|
||||||
|
|
||||||
if let Some(ref user) = self.user_info {
|
|
||||||
parts.push(format!("**User:** {}", user));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ref proj) = self.current_project {
|
|
||||||
let phase = proj.current_phase.as_deref().unwrap_or("unbekannt");
|
|
||||||
parts.push(format!("**Projekt:** {} (Phase: {})", proj.name, phase));
|
|
||||||
if let Some(ref dir) = proj.working_dir {
|
|
||||||
parts.push(format!("**Arbeitsverzeichnis:** {}", dir));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.active_credentials.is_empty() {
|
|
||||||
parts.push("**Verfügbare Zugänge (NICHT nachfragen!):**".to_string());
|
|
||||||
for cred in &self.active_credentials {
|
|
||||||
let db_part = cred.db_or_path.as_deref().map(|d| format!(" / {}", d)).unwrap_or_default();
|
|
||||||
parts.push(format!("- {}: {}{}", cred.name, cred.host, db_part));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.critical_rules.is_empty() {
|
|
||||||
parts.push("**Kritische Regeln:**".to_string());
|
|
||||||
for rule in &self.critical_rules {
|
|
||||||
parts.push(format!("- {}", rule));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if parts.is_empty() {
|
|
||||||
return String::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
format!("<critical-context>\n{}\n</critical-context>", parts.join("\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Geschätzte Token-Anzahl
|
|
||||||
pub fn estimate_tokens(&self) -> usize {
|
|
||||||
// Grobe Schätzung: ~4 Zeichen pro Token
|
|
||||||
self.render().len() / 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProjectContext {
|
|
||||||
/// Rendert Schicht 2 als System-Reminder
|
|
||||||
pub fn render(&self) -> String {
|
|
||||||
let mut parts = Vec::new();
|
|
||||||
|
|
||||||
if let Some(ref summary) = self.claude_md_summary {
|
|
||||||
parts.push(format!("**Projekt-Kontext:**\n{}", summary));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.decisions.is_empty() {
|
|
||||||
parts.push("**Getroffene Entscheidungen:**".to_string());
|
|
||||||
for dec in &self.decisions {
|
|
||||||
let reason = dec.reason.as_deref().map(|r| format!(" ({})", r)).unwrap_or_default();
|
|
||||||
parts.push(format!("- {}: {}{}", dec.topic, dec.decision, reason));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.open_todos.is_empty() {
|
|
||||||
parts.push("**Offene TODOs:**".to_string());
|
|
||||||
for todo in &self.open_todos {
|
|
||||||
parts.push(format!("- [ ] {}", todo));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.key_insights.is_empty() {
|
|
||||||
parts.push("**Wichtige Erkenntnisse:**".to_string());
|
|
||||||
for insight in &self.key_insights {
|
|
||||||
parts.push(format!("- {}", insight));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if parts.is_empty() {
|
|
||||||
return String::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
format!("<project-context>\n{}\n</project-context>", parts.join("\n\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Geschätzte Token-Anzahl
|
|
||||||
pub fn estimate_tokens(&self) -> usize {
|
|
||||||
self.render().len() / 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ Datenbank-Erweiterungen ============
|
|
||||||
|
|
||||||
impl Database {
|
|
||||||
/// Erstellt die Context-Tabellen
|
|
||||||
pub fn create_context_tables(&self) -> rusqlite::Result<()> {
|
|
||||||
self.conn.execute_batch(
|
|
||||||
"
|
|
||||||
-- Sticky Context (Schicht 1)
|
|
||||||
CREATE TABLE IF NOT EXISTS sticky_context (
|
|
||||||
key TEXT PRIMARY KEY,
|
|
||||||
value TEXT NOT NULL,
|
|
||||||
priority INTEGER DEFAULT 2,
|
|
||||||
updated_at TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Compacting-Archiv
|
|
||||||
CREATE TABLE IF NOT EXISTS compacting_archive (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
session_id TEXT NOT NULL,
|
|
||||||
extracted_at TEXT NOT NULL,
|
|
||||||
decisions TEXT,
|
|
||||||
open_questions TEXT,
|
|
||||||
key_insights TEXT,
|
|
||||||
mentioned_files TEXT,
|
|
||||||
mentioned_tools TEXT
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_archive_session ON compacting_archive(session_id);
|
|
||||||
|
|
||||||
-- Context-Failures (für Prompt-Optimierung)
|
|
||||||
CREATE TABLE IF NOT EXISTS context_failures (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
session_id TEXT NOT NULL,
|
|
||||||
timestamp TEXT NOT NULL,
|
|
||||||
context_type TEXT NOT NULL,
|
|
||||||
expected TEXT,
|
|
||||||
actual TEXT,
|
|
||||||
resolved INTEGER DEFAULT 0
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_failures_session ON context_failures(session_id);
|
|
||||||
"
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Speichert einen Sticky-Context-Eintrag
|
|
||||||
pub fn save_sticky_context(&self, key: &str, value: &str, priority: i32) -> rusqlite::Result<()> {
|
|
||||||
self.conn.execute(
|
|
||||||
"INSERT OR REPLACE INTO sticky_context (key, value, priority, updated_at)
|
|
||||||
VALUES (?1, ?2, ?3, ?4)",
|
|
||||||
rusqlite::params![key, value, priority, chrono::Local::now().to_rfc3339()],
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lädt alle Sticky-Context-Einträge
|
|
||||||
pub fn load_sticky_context(&self) -> rusqlite::Result<Vec<(String, String, i32)>> {
|
|
||||||
let mut stmt = self.conn.prepare(
|
|
||||||
"SELECT key, value, priority FROM sticky_context ORDER BY priority ASC"
|
|
||||||
)?;
|
|
||||||
let entries = stmt.query_map([], |row| {
|
|
||||||
Ok((row.get(0)?, row.get(1)?, row.get(2)?))
|
|
||||||
})?.collect::<rusqlite::Result<Vec<_>>>()?;
|
|
||||||
Ok(entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Löscht einen Sticky-Context-Eintrag
|
|
||||||
pub fn delete_sticky_context(&self, key: &str) -> rusqlite::Result<()> {
|
|
||||||
self.conn.execute("DELETE FROM sticky_context WHERE key = ?1", rusqlite::params![key])?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Archiviert extrahierten Kontext vor Compacting
|
|
||||||
pub fn archive_context(&self, extracted: &ExtractedContext) -> rusqlite::Result<()> {
|
|
||||||
self.conn.execute(
|
|
||||||
"INSERT INTO compacting_archive (id, session_id, extracted_at, decisions, open_questions, key_insights, mentioned_files, mentioned_tools)
|
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
|
||||||
rusqlite::params![
|
|
||||||
uuid::Uuid::new_v4().to_string(),
|
|
||||||
extracted.session_id,
|
|
||||||
extracted.extracted_at,
|
|
||||||
serde_json::to_string(&extracted.decisions).ok(),
|
|
||||||
serde_json::to_string(&extracted.open_questions).ok(),
|
|
||||||
serde_json::to_string(&extracted.key_insights).ok(),
|
|
||||||
serde_json::to_string(&extracted.mentioned_files).ok(),
|
|
||||||
serde_json::to_string(&extracted.mentioned_tools).ok(),
|
|
||||||
],
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lädt archivierten Kontext einer Session
|
|
||||||
pub fn load_archived_context(&self, session_id: &str) -> rusqlite::Result<Option<ExtractedContext>> {
|
|
||||||
let result = self.conn.query_row(
|
|
||||||
"SELECT session_id, extracted_at, decisions, open_questions, key_insights, mentioned_files, mentioned_tools
|
|
||||||
FROM compacting_archive WHERE session_id = ?1 ORDER BY extracted_at DESC LIMIT 1",
|
|
||||||
rusqlite::params![session_id],
|
|
||||||
|row| {
|
|
||||||
let decisions: Option<String> = row.get(2)?;
|
|
||||||
let questions: Option<String> = row.get(3)?;
|
|
||||||
let insights: Option<String> = row.get(4)?;
|
|
||||||
let files: Option<String> = row.get(5)?;
|
|
||||||
let tools: Option<String> = row.get(6)?;
|
|
||||||
|
|
||||||
Ok(ExtractedContext {
|
|
||||||
session_id: row.get(0)?,
|
|
||||||
extracted_at: row.get(1)?,
|
|
||||||
decisions: decisions.and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default(),
|
|
||||||
open_questions: questions.and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default(),
|
|
||||||
key_insights: insights.and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default(),
|
|
||||||
mentioned_files: files.and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default(),
|
|
||||||
mentioned_tools: tools.and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default(),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
);
|
|
||||||
match result {
|
|
||||||
Ok(ctx) => Ok(Some(ctx)),
|
|
||||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Speichert einen Context-Failure
|
|
||||||
pub fn log_context_failure(
|
|
||||||
&self,
|
|
||||||
session_id: &str,
|
|
||||||
context_type: &str,
|
|
||||||
expected: &str,
|
|
||||||
actual: &str,
|
|
||||||
) -> rusqlite::Result<()> {
|
|
||||||
self.conn.execute(
|
|
||||||
"INSERT INTO context_failures (id, session_id, timestamp, context_type, expected, actual)
|
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
|
||||||
rusqlite::params![
|
|
||||||
uuid::Uuid::new_v4().to_string(),
|
|
||||||
session_id,
|
|
||||||
chrono::Local::now().to_rfc3339(),
|
|
||||||
context_type,
|
|
||||||
expected,
|
|
||||||
actual,
|
|
||||||
],
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ Tauri Commands ============
|
|
||||||
|
|
||||||
/// Sticky Context laden (Schicht 1)
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn get_sticky_context(app: AppHandle) -> Result<StickyContext, String> {
|
|
||||||
let state = app.state::<DbState>();
|
|
||||||
let db = state.lock().unwrap();
|
|
||||||
|
|
||||||
// Context-Tabellen erstellen falls nicht vorhanden
|
|
||||||
let _ = db.create_context_tables();
|
|
||||||
|
|
||||||
let entries = db.load_sticky_context().map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
let mut ctx = StickyContext::default();
|
|
||||||
|
|
||||||
for (key, value, _priority) in entries {
|
|
||||||
match key.as_str() {
|
|
||||||
"user_info" => ctx.user_info = Some(value),
|
|
||||||
k if k.starts_with("cred:") => {
|
|
||||||
if let Ok(cred) = serde_json::from_str::<CredentialHint>(&value) {
|
|
||||||
ctx.active_credentials.push(cred);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
k if k.starts_with("project:") => {
|
|
||||||
if let Ok(proj) = serde_json::from_str::<ProjectInfo>(&value) {
|
|
||||||
ctx.current_project = Some(proj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
k if k.starts_with("rule:") => {
|
|
||||||
ctx.critical_rules.push(value);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sticky Context Eintrag setzen
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn set_sticky_context(
|
|
||||||
app: AppHandle,
|
|
||||||
key: String,
|
|
||||||
value: String,
|
|
||||||
priority: Option<i32>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let state = app.state::<DbState>();
|
|
||||||
let db = state.lock().unwrap();
|
|
||||||
|
|
||||||
let _ = db.create_context_tables();
|
|
||||||
|
|
||||||
db.save_sticky_context(&key, &value, priority.unwrap_or(2))
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
println!("📌 Sticky Context gesetzt: {}", key);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sticky Context Eintrag löschen
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn remove_sticky_context(app: AppHandle, key: String) -> Result<(), String> {
|
|
||||||
let state = app.state::<DbState>();
|
|
||||||
let db = state.lock().unwrap();
|
|
||||||
|
|
||||||
db.delete_sticky_context(&key).map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
println!("🗑️ Sticky Context entfernt: {}", key);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Projekt-Kontext laden (Schicht 2)
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn get_project_context(
|
|
||||||
app: AppHandle,
|
|
||||||
session_id: String,
|
|
||||||
) -> Result<ProjectContext, String> {
|
|
||||||
let state = app.state::<DbState>();
|
|
||||||
let db = state.lock().unwrap();
|
|
||||||
|
|
||||||
let _ = db.create_context_tables();
|
|
||||||
|
|
||||||
// Versuche archivierten Kontext zu laden
|
|
||||||
if let Ok(Some(archived)) = db.load_archived_context(&session_id) {
|
|
||||||
let mut ctx = ProjectContext::default();
|
|
||||||
ctx.decisions = archived.decisions;
|
|
||||||
ctx.key_insights = archived.key_insights;
|
|
||||||
ctx.open_todos = archived.open_questions; // open_questions als TODOs verwenden
|
|
||||||
return Ok(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ProjectContext::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Kontext vor Compacting extrahieren
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn extract_context_before_compacting(
|
|
||||||
app: AppHandle,
|
|
||||||
session_id: String,
|
|
||||||
messages_json: String,
|
|
||||||
) -> Result<ExtractedContext, String> {
|
|
||||||
// Nachrichten parsen
|
|
||||||
let messages: Vec<serde_json::Value> = serde_json::from_str(&messages_json)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
let mut extracted = ExtractedContext {
|
|
||||||
session_id: session_id.clone(),
|
|
||||||
extracted_at: chrono::Local::now().to_rfc3339(),
|
|
||||||
decisions: Vec::new(),
|
|
||||||
open_questions: Vec::new(),
|
|
||||||
key_insights: Vec::new(),
|
|
||||||
mentioned_files: Vec::new(),
|
|
||||||
mentioned_tools: Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Einfache Extraktion aus Nachrichten
|
|
||||||
for msg in &messages {
|
|
||||||
let content = msg.get("content").and_then(|c| c.as_str()).unwrap_or("");
|
|
||||||
|
|
||||||
// Entscheidungen finden (Muster: "Entscheidung:", "Wir nehmen", "Ich verwende")
|
|
||||||
if content.contains("Entscheidung:") || content.contains("Wir nehmen") || content.contains("Ich verwende") {
|
|
||||||
// Vereinfachte Extraktion
|
|
||||||
let lines: Vec<&str> = content.lines()
|
|
||||||
.filter(|l| l.contains("Entscheidung") || l.contains("verwende") || l.contains("nehmen"))
|
|
||||||
.take(3)
|
|
||||||
.collect();
|
|
||||||
for line in lines {
|
|
||||||
if line.len() > 10 && line.len() < 200 {
|
|
||||||
extracted.decisions.push(Decision {
|
|
||||||
topic: "Architektur".to_string(),
|
|
||||||
decision: line.trim().to_string(),
|
|
||||||
reason: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Offene Fragen finden (Muster: "TODO", "FIXME", "?")
|
|
||||||
if content.contains("TODO") || content.contains("FIXME") {
|
|
||||||
let lines: Vec<&str> = content.lines()
|
|
||||||
.filter(|l| l.contains("TODO") || l.contains("FIXME"))
|
|
||||||
.take(5)
|
|
||||||
.collect();
|
|
||||||
for line in lines {
|
|
||||||
if line.len() > 5 && line.len() < 200 {
|
|
||||||
extracted.open_questions.push(line.trim().to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dateien extrahieren (vereinfacht: Pfade mit /)
|
|
||||||
for word in content.split_whitespace() {
|
|
||||||
if word.contains('/') && word.contains('.') && word.len() < 100 {
|
|
||||||
let clean = word.trim_matches(|c| c == '"' || c == '\'' || c == '`' || c == '(' || c == ')');
|
|
||||||
if !extracted.mentioned_files.contains(&clean.to_string()) {
|
|
||||||
extracted.mentioned_files.push(clean.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Archivieren
|
|
||||||
let state = app.state::<DbState>();
|
|
||||||
let db = state.lock().unwrap();
|
|
||||||
let _ = db.create_context_tables();
|
|
||||||
let _ = db.archive_context(&extracted);
|
|
||||||
|
|
||||||
println!("📦 Kontext extrahiert: {} Entscheidungen, {} TODOs, {} Dateien",
|
|
||||||
extracted.decisions.len(),
|
|
||||||
extracted.open_questions.len(),
|
|
||||||
extracted.mentioned_files.len()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(extracted)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Context-Failure loggen (für Prompt-Optimierung)
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn log_context_failure(
|
|
||||||
app: AppHandle,
|
|
||||||
session_id: String,
|
|
||||||
context_type: String,
|
|
||||||
expected: String,
|
|
||||||
actual: String,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let state = app.state::<DbState>();
|
|
||||||
let db = state.lock().unwrap();
|
|
||||||
|
|
||||||
let _ = db.create_context_tables();
|
|
||||||
|
|
||||||
db.log_context_failure(&session_id, &context_type, &expected, &actual)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
println!("⚠️ Context-Failure geloggt: {} (erwartet: {}, tatsächlich: {})",
|
|
||||||
context_type, expected, actual);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Kombinierter System-Prompt aus Schicht 1+2
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn get_full_context(
|
|
||||||
app: AppHandle,
|
|
||||||
session_id: Option<String>,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
let state = app.state::<DbState>();
|
|
||||||
let db = state.lock().unwrap();
|
|
||||||
|
|
||||||
let _ = db.create_context_tables();
|
|
||||||
|
|
||||||
// Schicht 1: Sticky Context
|
|
||||||
let entries = db.load_sticky_context().map_err(|e| e.to_string())?;
|
|
||||||
let mut sticky = StickyContext::default();
|
|
||||||
|
|
||||||
for (key, value, _priority) in entries {
|
|
||||||
match key.as_str() {
|
|
||||||
"user_info" => sticky.user_info = Some(value),
|
|
||||||
k if k.starts_with("cred:") => {
|
|
||||||
if let Ok(cred) = serde_json::from_str::<CredentialHint>(&value) {
|
|
||||||
sticky.active_credentials.push(cred);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
k if k.starts_with("project:") => {
|
|
||||||
if let Ok(proj) = serde_json::from_str::<ProjectInfo>(&value) {
|
|
||||||
sticky.current_project = Some(proj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
k if k.starts_with("rule:") => {
|
|
||||||
sticky.critical_rules.push(value);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schicht 2: Projekt-Kontext (falls Session angegeben)
|
|
||||||
let mut project = ProjectContext::default();
|
|
||||||
if let Some(ref sid) = session_id {
|
|
||||||
if let Ok(Some(archived)) = db.load_archived_context(sid) {
|
|
||||||
project.decisions = archived.decisions;
|
|
||||||
project.key_insights = archived.key_insights;
|
|
||||||
project.open_todos = archived.open_questions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kombinieren
|
|
||||||
let sticky_rendered = sticky.render();
|
|
||||||
let project_rendered = project.render();
|
|
||||||
|
|
||||||
let mut full = String::new();
|
|
||||||
if !sticky_rendered.is_empty() {
|
|
||||||
full.push_str(&sticky_rendered);
|
|
||||||
}
|
|
||||||
if !project_rendered.is_empty() {
|
|
||||||
if !full.is_empty() {
|
|
||||||
full.push_str("\n\n");
|
|
||||||
}
|
|
||||||
full.push_str(&project_rendered);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(full)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Liste alle Sticky-Context-Einträge auf
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn list_sticky_context(
|
|
||||||
app: AppHandle,
|
|
||||||
) -> Result<Vec<(String, String, i32)>, String> {
|
|
||||||
let state = app.state::<DbState>();
|
|
||||||
let db = state.lock().unwrap();
|
|
||||||
|
|
||||||
let _ = db.create_context_tables();
|
|
||||||
|
|
||||||
db.load_sticky_context().map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
|
|
@ -10,7 +10,6 @@ use tauri::{
|
||||||
|
|
||||||
mod audit;
|
mod audit;
|
||||||
mod claude;
|
mod claude;
|
||||||
mod context;
|
|
||||||
mod db;
|
mod db;
|
||||||
mod guard;
|
mod guard;
|
||||||
mod knowledge;
|
mod knowledge;
|
||||||
|
|
@ -75,15 +74,6 @@ pub fn run() {
|
||||||
knowledge::get_knowledge_categories,
|
knowledge::get_knowledge_categories,
|
||||||
knowledge::get_recent_knowledge,
|
knowledge::get_recent_knowledge,
|
||||||
knowledge::test_knowledge_connection,
|
knowledge::test_knowledge_connection,
|
||||||
// Context-Management
|
|
||||||
context::get_sticky_context,
|
|
||||||
context::set_sticky_context,
|
|
||||||
context::remove_sticky_context,
|
|
||||||
context::get_project_context,
|
|
||||||
context::extract_context_before_compacting,
|
|
||||||
context::log_context_failure,
|
|
||||||
context::get_full_context,
|
|
||||||
context::list_sticky_context,
|
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let handle = app.handle().clone();
|
let handle = app.handle().clone();
|
||||||
|
|
|
||||||
|
|
@ -1,570 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
|
||||||
|
|
||||||
// Typen
|
|
||||||
interface StickyEntry {
|
|
||||||
key: string;
|
|
||||||
value: string;
|
|
||||||
priority: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CredentialHint {
|
|
||||||
name: string;
|
|
||||||
host: string;
|
|
||||||
db_or_path: string | null;
|
|
||||||
inject_pattern: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProjectInfo {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
current_phase: string | null;
|
|
||||||
working_dir: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// State
|
|
||||||
let entries = $state<StickyEntry[]>([]);
|
|
||||||
let loading = $state(false);
|
|
||||||
let fullContext = $state('');
|
|
||||||
let showAddDialog = $state(false);
|
|
||||||
let showPreview = $state(false);
|
|
||||||
|
|
||||||
// Neuer Eintrag
|
|
||||||
let newEntry = $state({
|
|
||||||
type: 'rule',
|
|
||||||
key: '',
|
|
||||||
value: '',
|
|
||||||
priority: 2
|
|
||||||
});
|
|
||||||
|
|
||||||
// Vordefinierte Entry-Typen
|
|
||||||
const entryTypes = [
|
|
||||||
{ id: 'user_info', label: 'User-Info', prefix: 'user_info', icon: '👤' },
|
|
||||||
{ id: 'rule', label: 'Kritische Regel', prefix: 'rule:', icon: '⚠️' },
|
|
||||||
{ id: 'cred', label: 'Zugang (Credential)', prefix: 'cred:', icon: '🔑' },
|
|
||||||
{ id: 'project', label: 'Projekt-Info', prefix: 'project:', icon: '📁' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Prioritäts-Labels
|
|
||||||
const priorityLabels: Record<number, string> = {
|
|
||||||
1: 'Kritisch (immer zuerst)',
|
|
||||||
2: 'Hoch',
|
|
||||||
3: 'Normal',
|
|
||||||
4: 'Niedrig'
|
|
||||||
};
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await loadEntries();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadEntries() {
|
|
||||||
loading = true;
|
|
||||||
try {
|
|
||||||
const result: [string, string, number][] = await invoke('list_sticky_context');
|
|
||||||
entries = result.map(([key, value, priority]) => ({ key, value, priority }));
|
|
||||||
|
|
||||||
// Auch den gerenderten Context laden
|
|
||||||
fullContext = await invoke('get_full_context', { sessionId: null });
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Fehler beim Laden:', err);
|
|
||||||
}
|
|
||||||
loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addEntry() {
|
|
||||||
if (!newEntry.value.trim()) return;
|
|
||||||
|
|
||||||
const type = entryTypes.find(t => t.id === newEntry.type);
|
|
||||||
let key = '';
|
|
||||||
|
|
||||||
if (newEntry.type === 'user_info') {
|
|
||||||
key = 'user_info';
|
|
||||||
} else if (newEntry.type === 'cred') {
|
|
||||||
// Credential als JSON speichern
|
|
||||||
const cred: CredentialHint = {
|
|
||||||
name: newEntry.key || 'Unbenannt',
|
|
||||||
host: newEntry.value,
|
|
||||||
db_or_path: null,
|
|
||||||
inject_pattern: null
|
|
||||||
};
|
|
||||||
key = `cred:${newEntry.key || 'default'}`;
|
|
||||||
newEntry.value = JSON.stringify(cred);
|
|
||||||
} else if (newEntry.type === 'project') {
|
|
||||||
const proj: ProjectInfo = {
|
|
||||||
id: newEntry.key || 'default',
|
|
||||||
name: newEntry.value,
|
|
||||||
current_phase: null,
|
|
||||||
working_dir: null
|
|
||||||
};
|
|
||||||
key = `project:${newEntry.key || 'current'}`;
|
|
||||||
newEntry.value = JSON.stringify(proj);
|
|
||||||
} else {
|
|
||||||
key = `${type?.prefix || ''}${newEntry.key || Date.now()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await invoke('set_sticky_context', {
|
|
||||||
key,
|
|
||||||
value: newEntry.value,
|
|
||||||
priority: newEntry.priority
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset und neu laden
|
|
||||||
newEntry = { type: 'rule', key: '', value: '', priority: 2 };
|
|
||||||
showAddDialog = false;
|
|
||||||
await loadEntries();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Fehler beim Speichern:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeEntry(key: string) {
|
|
||||||
try {
|
|
||||||
await invoke('remove_sticky_context', { key });
|
|
||||||
await loadEntries();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Fehler beim Löschen:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEntryIcon(key: string): string {
|
|
||||||
if (key === 'user_info') return '👤';
|
|
||||||
if (key.startsWith('rule:')) return '⚠️';
|
|
||||||
if (key.startsWith('cred:')) return '🔑';
|
|
||||||
if (key.startsWith('project:')) return '📁';
|
|
||||||
return '📌';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEntryLabel(key: string): string {
|
|
||||||
if (key === 'user_info') return 'User-Info';
|
|
||||||
if (key.startsWith('rule:')) return key.replace('rule:', '');
|
|
||||||
if (key.startsWith('cred:')) return key.replace('cred:', '');
|
|
||||||
if (key.startsWith('project:')) return key.replace('project:', '');
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatValue(key: string, value: string): string {
|
|
||||||
// JSON-Werte formatieren
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(value);
|
|
||||||
if (key.startsWith('cred:')) {
|
|
||||||
return `${parsed.host}${parsed.db_or_path ? ' / ' + parsed.db_or_path : ''}`;
|
|
||||||
}
|
|
||||||
if (key.startsWith('project:')) {
|
|
||||||
return `${parsed.name}${parsed.current_phase ? ' (' + parsed.current_phase + ')' : ''}`;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Kein JSON, Wert direkt zurückgeben
|
|
||||||
}
|
|
||||||
return value.length > 100 ? value.slice(0, 100) + '...' : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function estimateTokens(text: string): number {
|
|
||||||
return Math.ceil(text.length / 4);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="context-panel">
|
|
||||||
<div class="panel-header">
|
|
||||||
<h2>📌 Sticky Context</h2>
|
|
||||||
<div class="header-actions">
|
|
||||||
<button class="btn-preview" onclick={() => showPreview = !showPreview}>
|
|
||||||
{showPreview ? '📝 Liste' : '👁️ Vorschau'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-box">
|
|
||||||
<strong>Schicht 1:</strong> Diese Einträge werden bei JEDEM API-Call an Claude gesendet.
|
|
||||||
<br>
|
|
||||||
<span class="token-count">~{estimateTokens(fullContext)} Token</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if showPreview}
|
|
||||||
<!-- Vorschau des gerenderten Contexts -->
|
|
||||||
<div class="preview-box">
|
|
||||||
<pre>{fullContext || '(Kein Context konfiguriert)'}</pre>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<!-- Eintrags-Liste -->
|
|
||||||
<div class="entries-list">
|
|
||||||
{#if loading}
|
|
||||||
<div class="loading">Lade...</div>
|
|
||||||
{:else if entries.length === 0}
|
|
||||||
<div class="empty-state">
|
|
||||||
Noch keine Einträge. Füge kritische Informationen hinzu,
|
|
||||||
die Claude immer kennen soll.
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
{#each entries as entry}
|
|
||||||
<div class="entry-item">
|
|
||||||
<div class="entry-header">
|
|
||||||
<span class="entry-icon">{getEntryIcon(entry.key)}</span>
|
|
||||||
<span class="entry-key">{getEntryLabel(entry.key)}</span>
|
|
||||||
<span class="entry-priority" class:critical={entry.priority === 1}>
|
|
||||||
P{entry.priority}
|
|
||||||
</span>
|
|
||||||
<button class="btn-delete" onclick={() => removeEntry(entry.key)}>✕</button>
|
|
||||||
</div>
|
|
||||||
<div class="entry-value">{formatValue(entry.key, entry.value)}</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
<button class="btn-add" onclick={() => showAddDialog = true}>
|
|
||||||
+ Eintrag hinzufügen
|
|
||||||
</button>
|
|
||||||
<button class="btn-refresh" onclick={loadEntries} disabled={loading}>
|
|
||||||
🔄 Aktualisieren
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Dialog: Neuer Eintrag -->
|
|
||||||
{#if showAddDialog}
|
|
||||||
<div class="modal-overlay" onclick={() => showAddDialog = false}>
|
|
||||||
<div class="modal-content" onclick={(e) => e.stopPropagation()}>
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3>📌 Neuer Sticky-Context-Eintrag</h3>
|
|
||||||
<button class="btn-close" onclick={() => showAddDialog = false}>✕</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="entry-type">Typ</label>
|
|
||||||
<select id="entry-type" bind:value={newEntry.type}>
|
|
||||||
{#each entryTypes as type}
|
|
||||||
<option value={type.id}>{type.icon} {type.label}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if newEntry.type !== 'user_info'}
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="entry-key">
|
|
||||||
{#if newEntry.type === 'rule'}Name der Regel{:else if newEntry.type === 'cred'}Name des Zugangs{:else}ID{/if}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="entry-key"
|
|
||||||
type="text"
|
|
||||||
bind:value={newEntry.key}
|
|
||||||
placeholder={newEntry.type === 'cred' ? 'z.B. dolibarr-test' : 'z.B. keine-force-push'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="entry-value">
|
|
||||||
{#if newEntry.type === 'user_info'}User-Info
|
|
||||||
{:else if newEntry.type === 'cred'}Host (z.B. 192.168.155.11)
|
|
||||||
{:else if newEntry.type === 'project'}Projektname
|
|
||||||
{:else}Regel-Text{/if}
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="entry-value"
|
|
||||||
bind:value={newEntry.value}
|
|
||||||
rows="3"
|
|
||||||
placeholder={newEntry.type === 'user_info' ? 'Eddy, Elektroinstallationsbetrieb ALLES WATT LÄUFT' : ''}
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="entry-priority">Priorität</label>
|
|
||||||
<select id="entry-priority" bind:value={newEntry.priority}>
|
|
||||||
{#each Object.entries(priorityLabels) as [val, label]}
|
|
||||||
<option value={Number(val)}>{label}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-actions">
|
|
||||||
<button class="btn-cancel" onclick={() => showAddDialog = false}>
|
|
||||||
Abbrechen
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn-confirm"
|
|
||||||
onclick={addEntry}
|
|
||||||
disabled={!newEntry.value.trim()}
|
|
||||||
>
|
|
||||||
💾 Speichern
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.context-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-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-header h2 {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-preview {
|
|
||||||
padding: 2px 8px;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 0.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-box {
|
|
||||||
padding: var(--spacing-sm);
|
|
||||||
background: rgba(59, 130, 246, 0.1);
|
|
||||||
border: 1px solid var(--accent);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-count {
|
|
||||||
color: var(--accent);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entries-list {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading, .empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--spacing-xl);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-item {
|
|
||||||
padding: var(--spacing-sm);
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-xs);
|
|
||||||
margin-bottom: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-icon {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-key {
|
|
||||||
flex: 1;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-priority {
|
|
||||||
font-size: 0.65rem;
|
|
||||||
padding: 1px 4px;
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-priority.critical {
|
|
||||||
background: var(--error);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-delete {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: transparent;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-delete:hover {
|
|
||||||
background: var(--error);
|
|
||||||
color: white;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-value {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-box {
|
|
||||||
flex: 1;
|
|
||||||
overflow: auto;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
padding: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-box pre {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
margin-top: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-add {
|
|
||||||
flex: 1;
|
|
||||||
padding: var(--spacing-sm);
|
|
||||||
background: var(--accent);
|
|
||||||
color: white;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-refresh {
|
|
||||||
padding: var(--spacing-sm);
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal */
|
|
||||||
.modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.7);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background: var(--bg-primary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
max-width: 450px;
|
|
||||||
width: 90%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: var(--spacing-md);
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header h3 {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-close {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input,
|
|
||||||
.form-group select,
|
|
||||||
.form-group textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: var(--spacing-sm);
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group textarea {
|
|
||||||
resize: vertical;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: var(--spacing-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cancel {
|
|
||||||
padding: var(--spacing-sm) var(--spacing-md);
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-confirm {
|
|
||||||
padding: var(--spacing-sm) var(--spacing-md);
|
|
||||||
background: var(--accent);
|
|
||||||
color: white;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-confirm:disabled {
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
import AgentView from '$lib/components/AgentView.svelte';
|
import AgentView from '$lib/components/AgentView.svelte';
|
||||||
import MemoryPanel from '$lib/components/MemoryPanel.svelte';
|
import MemoryPanel from '$lib/components/MemoryPanel.svelte';
|
||||||
import KnowledgePanel from '$lib/components/KnowledgePanel.svelte';
|
import KnowledgePanel from '$lib/components/KnowledgePanel.svelte';
|
||||||
import ContextPanel from '$lib/components/ContextPanel.svelte';
|
|
||||||
import AuditLog from '$lib/components/AuditLog.svelte';
|
import AuditLog from '$lib/components/AuditLog.svelte';
|
||||||
import GuardRailsPanel from '$lib/components/GuardRailsPanel.svelte';
|
import GuardRailsPanel from '$lib/components/GuardRailsPanel.svelte';
|
||||||
import SettingsPanel from '$lib/components/SettingsPanel.svelte';
|
import SettingsPanel from '$lib/components/SettingsPanel.svelte';
|
||||||
|
|
@ -25,7 +24,6 @@
|
||||||
|
|
||||||
const rightTabs = [
|
const rightTabs = [
|
||||||
{ id: 'agents', label: 'Agents', icon: '🤖' },
|
{ id: 'agents', label: 'Agents', icon: '🤖' },
|
||||||
{ id: 'context', label: 'Context', icon: '📌' },
|
|
||||||
{ id: 'guards', label: 'Guard-Rails', icon: '🛡️' },
|
{ id: 'guards', label: 'Guard-Rails', icon: '🛡️' },
|
||||||
{ id: 'settings', label: 'Settings', icon: '⚙️' },
|
{ id: 'settings', label: 'Settings', icon: '⚙️' },
|
||||||
];
|
];
|
||||||
|
|
@ -98,8 +96,6 @@
|
||||||
<div class="panel-content">
|
<div class="panel-content">
|
||||||
{#if activeRightTab === 'agents'}
|
{#if activeRightTab === 'agents'}
|
||||||
<AgentView />
|
<AgentView />
|
||||||
{:else if activeRightTab === 'context'}
|
|
||||||
<ContextPanel />
|
|
||||||
{:else if activeRightTab === 'guards'}
|
{:else if activeRightTab === 'guards'}
|
||||||
<GuardRailsPanel />
|
<GuardRailsPanel />
|
||||||
{:else if activeRightTab === 'settings'}
|
{:else if activeRightTab === 'settings'}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue