Sticky Context Auto-Load beim App-Start

- init_sticky_context Tauri Command: Lädt Context aus DB, sendet an Bridge
- Frontend ruft Command beim Start auf (+layout.svelte)
- StickyContextInfo Store für Status-Tracking
- Context-Badge im Footer (📌 +XXctx Token)
- Zeigt Anzahl Einträge und Token-Schätzung

Bugfixes:
- context.rs: Typ-Annotationen in Closures (String statt str)
- db.rs: conn als pub(crate) für Module-Zugriff
- memory.rs: get_sticky_context → get_sticky_memory_entries (Namenskonflikt)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-14 14:39:39 +02:00
parent 0d292179e2
commit 51239d6639
9 changed files with 1089 additions and 22 deletions

View file

@ -180,9 +180,13 @@ Die App hat keinen direkten Zugriff auf die zentrale Wissensbasis (`claude` DB a
- ✅ **"Das merken" im Chat** — 💡 Button bei Nachrichten, Modal-Dialog (56eb2f5) - ✅ **"Das merken" im Chat** — 💡 Button bei Nachrichten, Modal-Dialog (56eb2f5)
### Noch offen (niedrigere Priorität) ### Nachträglich implementiert (14.04.2026)
- [ ] **Sticky Context** — Automatisch beim Chat-Start laden - ✅ **Sticky Context Auto-Load** — Context wird beim App-Start automatisch geladen und an die Bridge gesendet
- `init_sticky_context` Tauri Command erstellt
- Frontend ruft Command in `+layout.svelte` auf
- Context-Status im Footer angezeigt (📌 +XXctx)
- Enthält Anzahl Einträge und Token-Schätzung
### Verifikation ### Verifikation
```bash ```bash

948
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -520,3 +520,82 @@ pub struct ModelInfo {
pub name: String, pub name: String,
pub description: String, pub description: String,
} }
/// Sticky Context Initialisierungs-Info
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct StickyContextInfo {
pub loaded: bool,
pub entries: usize,
pub estimated_tokens: usize,
pub has_user_info: bool,
pub has_project: bool,
pub credentials_count: usize,
pub rules_count: usize,
}
/// Sticky Context beim App-Start initialisieren
/// Lädt den Context aus der DB und sendet ihn an die Bridge
#[tauri::command]
pub async fn init_sticky_context(app: AppHandle) -> Result<StickyContextInfo, String> {
println!("📌 Initialisiere Sticky Context...");
// Context aus DB laden
let context = load_sticky_context_for_prompt(&app);
let mut info = StickyContextInfo {
loaded: false,
entries: 0,
estimated_tokens: 0,
has_user_info: false,
has_project: false,
credentials_count: 0,
rules_count: 0,
};
// Details aus DB laden für Info
if let Some(db_state) = app.try_state::<Arc<Mutex<crate::db::Database>>>() {
let db = db_state.lock().unwrap();
let _ = db.create_context_tables();
if let Ok(entries) = db.load_sticky_context() {
info.entries = entries.len();
for (key, _value, _priority) in &entries {
match key.as_str() {
"user_info" => info.has_user_info = true,
k if k.starts_with("cred:") => info.credentials_count += 1,
k if k.starts_with("project:") => info.has_project = true,
k if k.starts_with("rule:") => info.rules_count += 1,
_ => {}
}
}
}
}
if let Some(ref ctx) = context {
info.loaded = true;
info.estimated_tokens = ctx.len() / 4;
// Bridge starten falls nicht aktiv
let needs_start = {
let state = app.state::<Arc<Mutex<ClaudeState>>>();
let state_guard = state.lock().unwrap();
state_guard.bridge_stdin.is_none()
};
if needs_start {
start_bridge(&app)?;
// Kurz warten bis Bridge bereit
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
}
// Context an Bridge senden
let _ = send_to_bridge(&app, "set-context", ctx);
println!("✅ Sticky Context geladen: {} Einträge, ~{} Token", info.entries, info.estimated_tokens);
} else {
println!(" Kein Sticky Context konfiguriert");
}
Ok(info)
}

View file

@ -278,11 +278,11 @@ impl Database {
Ok(ExtractedContext { Ok(ExtractedContext {
session_id: row.get(0)?, session_id: row.get(0)?,
extracted_at: row.get(1)?, extracted_at: row.get(1)?,
decisions: decisions.and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default(), decisions: decisions.and_then(|s: String| serde_json::from_str(&s).ok()).unwrap_or_default(),
open_questions: questions.and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default(), open_questions: questions.and_then(|s: String| serde_json::from_str(&s).ok()).unwrap_or_default(),
key_insights: insights.and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default(), key_insights: insights.and_then(|s: String| 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_files: files.and_then(|s: String| serde_json::from_str(&s).ok()).unwrap_or_default(),
mentioned_tools: tools.and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default(), mentioned_tools: tools.and_then(|s: String| serde_json::from_str(&s).ok()).unwrap_or_default(),
}) })
}, },
); );

View file

@ -55,7 +55,7 @@ pub struct MonitorEvent {
/// Datenbank-Wrapper /// Datenbank-Wrapper
pub struct Database { pub struct Database {
conn: Connection, pub(crate) conn: Connection,
} }
/// Datenbank-Statistiken /// Datenbank-Statistiken

View file

@ -32,9 +32,10 @@ pub fn run() {
claude::set_model, claude::set_model,
claude::get_available_models, claude::get_available_models,
claude::get_current_model, claude::get_current_model,
claude::init_sticky_context,
// Gedächtnis-System // Gedächtnis-System
memory::load_memory, memory::load_memory,
memory::get_sticky_context, memory::get_sticky_memory_entries,
memory::save_pattern, memory::save_pattern,
memory::detect_issue, memory::detect_issue,
// Audit-Log // Audit-Log

View file

@ -147,9 +147,9 @@ pub async fn load_memory(app: AppHandle) -> Result<MemoryStats, String> {
}) })
} }
/// Holt den Sticky-Kontext für Claude /// Holt die Sticky-Memory-Einträge (veraltet, nutze context::get_sticky_context)
#[tauri::command] #[tauri::command]
pub async fn get_sticky_context(app: AppHandle) -> Result<Vec<MemoryEntry>, String> { pub async fn get_sticky_memory_entries(app: AppHandle) -> Result<Vec<MemoryEntry>, String> {
let state = app.state::<Arc<Mutex<db::Database>>>(); let state = app.state::<Arc<Mutex<db::Database>>>();
let db_lock = state.lock().unwrap(); let db_lock = state.lock().unwrap();
let entries = db_lock.load_memory_entries().map_err(|e| e.to_string())?; let entries = db_lock.load_memory_entries().map_err(|e| e.to_string())?;

View file

@ -66,6 +66,19 @@ export const sessionStats = writable({
messageCount: 0, messageCount: 0,
}); });
// Sticky Context Status (beim App-Start geladen)
export interface StickyContextInfo {
loaded: boolean;
entries: number;
estimatedTokens: number;
hasUserInfo: boolean;
hasProject: boolean;
credentialsCount: number;
rulesCount: number;
}
export const stickyContextInfo = writable<StickyContextInfo | null>(null);
// Abgeleitete Stores // Abgeleitete Stores
export const activeAgents = derived(agents, ($agents) => export const activeAgents = derived(agents, ($agents) =>
$agents.filter((a) => a.status === 'active') $agents.filter((a) => a.status === 'active')

View file

@ -2,7 +2,7 @@
import '../app.css'; import '../app.css';
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { isProcessing, agentCount, currentModel, sessionStats, initEventListeners, cleanupEventListeners, currentSessionId, setMessagesFromDb, type DbMessage } from '$lib/stores'; import { isProcessing, agentCount, currentModel, sessionStats, initEventListeners, cleanupEventListeners, currentSessionId, setMessagesFromDb, stickyContextInfo, type DbMessage, type StickyContextInfo } from '$lib/stores';
import StopButton from '$lib/components/StopButton.svelte'; import StopButton from '$lib/components/StopButton.svelte';
// Session-Typ vom Backend // Session-Typ vom Backend
@ -21,6 +21,17 @@
last_message: string | null; last_message: string | null;
} }
// Backend-Response für Sticky Context
interface StickyContextResponse {
loaded: boolean;
entries: number;
estimated_tokens: number;
has_user_info: boolean;
has_project: boolean;
credentials_count: number;
rules_count: number;
}
onMount(async () => { onMount(async () => {
await initEventListeners(); await initEventListeners();
@ -34,6 +45,25 @@
console.warn('Modell konnte nicht geladen werden:', err); console.warn('Modell konnte nicht geladen werden:', err);
} }
// Sticky Context beim Start laden und an Bridge senden
try {
const ctx: StickyContextResponse = await invoke('init_sticky_context');
$stickyContextInfo = {
loaded: ctx.loaded,
entries: ctx.entries,
estimatedTokens: ctx.estimated_tokens,
hasUserInfo: ctx.has_user_info,
hasProject: ctx.has_project,
credentialsCount: ctx.credentials_count,
rulesCount: ctx.rules_count,
};
if (ctx.loaded) {
console.log(`📌 Sticky Context geladen: ${ctx.entries} Einträge, ~${ctx.estimated_tokens} Token`);
}
} catch (err) {
console.warn('Sticky Context konnte nicht geladen werden:', err);
}
// Aktive Session automatisch laden (falls vorhanden) // Aktive Session automatisch laden (falls vorhanden)
try { try {
const activeSession: Session | null = await invoke('get_active_session'); const activeSession: Session | null = await invoke('get_active_session');
@ -125,6 +155,12 @@
<footer class="footer" class:active={$isProcessing}> <footer class="footer" class:active={$isProcessing}>
<StopButton on:click={handleStop} disabled={!$isProcessing} /> <StopButton on:click={handleStop} disabled={!$isProcessing} />
<div class="footer-stats"> <div class="footer-stats">
{#if $stickyContextInfo?.loaded}
<span class="context-badge" title="Sticky Context: {$stickyContextInfo.entries} Einträge, ~{$stickyContextInfo.estimatedTokens} Token">
📌 +{$stickyContextInfo.estimatedTokens}ctx
</span>
<span class="sep">|</span>
{/if}
<span>Token: {formatTokens($sessionStats.totalTokensIn)} in / {formatTokens($sessionStats.totalTokensOut)} out</span> <span>Token: {formatTokens($sessionStats.totalTokensIn)} in / {formatTokens($sessionStats.totalTokensOut)} out</span>
<span class="sep">|</span> <span class="sep">|</span>
<span>Kosten: {formatCost($sessionStats.totalCost)}</span> <span>Kosten: {formatCost($sessionStats.totalCost)}</span>
@ -242,4 +278,10 @@
color: var(--accent); color: var(--accent);
font-weight: 600; font-weight: 600;
} }
.footer-stats .context-badge {
color: #22c55e;
font-weight: 500;
cursor: help;
}
</style> </style>