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:
parent
0d292179e2
commit
51239d6639
9 changed files with 1089 additions and 22 deletions
|
|
@ -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
948
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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())?;
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue