diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index b6cc478..97ff1d5 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -35,6 +35,7 @@ pub struct ChatMessage { pub role: String, // "user", "assistant", "system" pub content: String, pub model: Option, + pub agent_id: Option, // Agent der die Nachricht erzeugt hat pub timestamp: String, } @@ -160,10 +161,13 @@ impl Database { role TEXT NOT NULL, content TEXT NOT NULL, model TEXT, + agent_id TEXT, timestamp TEXT NOT NULL, FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp); + + " ", )?; Ok(()) @@ -517,19 +521,51 @@ impl Database { Ok(()) } + /// Holt die zuletzt aktualisierte aktive Session + pub fn get_active_session(&self) -> SqlResult> { + let result = self.conn.query_row( + "SELECT id, claude_session_id, title, working_dir, message_count, + token_input, token_output, cost_usd, status, created_at, updated_at, last_message + FROM sessions WHERE status = 'active' ORDER BY updated_at DESC LIMIT 1", + [], + |row| { + Ok(Session { + id: row.get(0)?, + claude_session_id: row.get(1)?, + title: row.get(2)?, + working_dir: row.get(3)?, + message_count: row.get(4)?, + token_input: row.get(5)?, + token_output: row.get(6)?, + cost_usd: row.get(7)?, + status: row.get(8)?, + created_at: row.get(9)?, + updated_at: row.get(10)?, + last_message: row.get(11)?, + }) + }, + ); + match result { + Ok(s) => Ok(Some(s)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e), + } + } + // ============ Messages ============ /// Speichert eine Nachricht pub fn save_message(&self, msg: &ChatMessage) -> SqlResult<()> { self.conn.execute( - "INSERT OR REPLACE INTO messages (id, session_id, role, content, model, timestamp) - VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + "INSERT OR REPLACE INTO messages (id, session_id, role, content, model, agent_id, timestamp) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", params![ msg.id, msg.session_id, msg.role, msg.content, msg.model, + msg.agent_id, msg.timestamp, ], )?; @@ -539,7 +575,7 @@ impl Database { /// Lädt alle Nachrichten einer Session pub fn load_messages(&self, session_id: &str) -> SqlResult> { let mut stmt = self.conn.prepare( - "SELECT id, session_id, role, content, model, timestamp + "SELECT id, session_id, role, content, model, agent_id, timestamp FROM messages WHERE session_id = ?1 ORDER BY timestamp ASC" )?; @@ -550,7 +586,8 @@ impl Database { role: row.get(2)?, content: row.get(3)?, model: row.get(4)?, - timestamp: row.get(5)?, + agent_id: row.get(5)?, + timestamp: row.get(6)?, }) })?.collect::>>()?; @@ -563,6 +600,83 @@ impl Database { Ok(()) } + /// Zählt Nachrichten einer Session + pub fn count_messages(&self, session_id: &str) -> SqlResult { + self.conn.query_row( + "SELECT COUNT(*) FROM messages WHERE session_id = ?1", + params![session_id], + |row| row.get(0), + ) + } + + /// Kompaktiert eine Session: Behält die letzten N Nachrichten, fasst ältere zusammen + /// Gibt die Anzahl der kompaktierten Nachrichten zurück + pub fn compact_session(&self, session_id: &str, keep_last: usize) -> SqlResult { + let total = self.count_messages(session_id)?; + if total <= keep_last { + return Ok(0); // Nichts zu kompaktieren + } + + let to_compact = total - keep_last; + + // Alte Nachrichten holen (die kompaktiert werden sollen) + let mut stmt = self.conn.prepare( + "SELECT id, role, content FROM messages + WHERE session_id = ?1 + ORDER BY timestamp ASC + LIMIT ?2" + )?; + + let old_messages: Vec<(String, String, String)> = stmt.query_map( + params![session_id, to_compact as i64], + |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)) + )?.collect::>>()?; + + if old_messages.is_empty() { + return Ok(0); + } + + // Summary erstellen + let mut summary_parts: Vec = Vec::new(); + for (_, role, content) in &old_messages { + let preview = if content.len() > 200 { + format!("{}...", &content[..200]) + } else { + content.clone() + }; + summary_parts.push(format!("[{}] {}", role, preview)); + } + let summary_content = format!( + "📦 **Kompaktierter Kontext** ({} Nachrichten)\n\n{}", + old_messages.len(), + summary_parts.join("\n\n---\n\n") + ); + + // IDs der alten Nachrichten + let old_ids: Vec<&str> = old_messages.iter().map(|(id, _, _)| id.as_str()).collect(); + + // Alte Nachrichten löschen + let placeholders: String = old_ids.iter().map(|_| "?").collect::>().join(","); + let delete_sql = format!("DELETE FROM messages WHERE id IN ({})", placeholders); + + let params: Vec<&dyn rusqlite::ToSql> = old_ids.iter().map(|id| id as &dyn rusqlite::ToSql).collect(); + self.conn.execute(&delete_sql, params.as_slice())?; + + // Summary-Nachricht einfügen (mit Timestamp vor allen anderen) + let summary_msg = ChatMessage { + id: uuid::Uuid::new_v4().to_string(), + session_id: session_id.to_string(), + role: "system".to_string(), + content: summary_content, + model: None, + agent_id: None, + timestamp: "1970-01-01T00:00:00Z".to_string(), // Ganz am Anfang + }; + self.save_message(&summary_msg)?; + + Ok(old_messages.len()) + } + // ============ Settings ============ /// Speichert eine Einstellung @@ -732,3 +846,22 @@ pub async fn clear_messages(app: AppHandle, session_id: String) -> Result<(), St let db = state.lock().unwrap(); db.clear_messages(&session_id).map_err(|e| e.to_string()) } + +/// Session kompaktieren — fasst alte Nachrichten zusammen +#[tauri::command] +pub async fn compact_session( + app: AppHandle, + session_id: String, + keep_last: Option, +) -> Result { + let keep = keep_last.unwrap_or(30); // Standard: letzte 30 Nachrichten behalten + let state = app.state::(); + let db = state.lock().unwrap(); + let compacted = db.compact_session(&session_id, keep).map_err(|e| e.to_string())?; + + if compacted > 0 { + println!("📦 Session {} kompaktiert: {} Nachrichten zusammengefasst", session_id, compacted); + } + + Ok(compacted) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 02c6d49..02d014e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -65,6 +65,7 @@ pub fn run() { db::save_message, db::load_messages, db::clear_messages, + db::compact_session, ]) .setup(|app| { let handle = app.handle().clone(); diff --git a/src/lib/stores/app.ts b/src/lib/stores/app.ts index eddbb7e..99ff654 100644 --- a/src/lib/stores/app.ts +++ b/src/lib/stores/app.ts @@ -231,6 +231,7 @@ export interface DbMessage { role: string; content: string; model: string | null; + agent_id: string | null; // Agent der die Nachricht erzeugt hat timestamp: string; } @@ -242,6 +243,7 @@ export function messageToDb(msg: Message, sessionId: string): DbMessage { role: msg.role, content: msg.content, model: msg.model || null, + agent_id: msg.agentId || null, timestamp: msg.timestamp.toISOString(), }; } @@ -253,6 +255,7 @@ export function dbToMessage(db: DbMessage): Message { role: db.role as Message['role'], content: db.content, model: db.model || undefined, + agentId: db.agent_id || undefined, timestamp: new Date(db.timestamp), }; } diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 83ddb6e..94c62f4 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -2,9 +2,25 @@ import '../app.css'; import { onMount, onDestroy } from 'svelte'; import { invoke } from '@tauri-apps/api/core'; - import { isProcessing, agentCount, currentModel, sessionStats, initEventListeners, cleanupEventListeners } from '$lib/stores'; + import { isProcessing, agentCount, currentModel, sessionStats, initEventListeners, cleanupEventListeners, currentSessionId, setMessagesFromDb, type DbMessage } from '$lib/stores'; import StopButton from '$lib/components/StopButton.svelte'; + // Session-Typ vom Backend + interface Session { + id: string; + claude_session_id: string | null; + title: string; + working_dir: string | null; + message_count: number; + token_input: number; + token_output: number; + cost_usd: number; + status: string; + created_at: string; + updated_at: string; + last_message: string | null; + } + onMount(async () => { await initEventListeners(); @@ -17,6 +33,32 @@ } catch (err) { console.warn('Modell konnte nicht geladen werden:', err); } + + // Aktive Session automatisch laden (falls vorhanden) + try { + const activeSession: Session | null = await invoke('get_active_session'); + if (activeSession) { + console.log('📂 Lade aktive Session:', activeSession.title); + $currentSessionId = activeSession.id; + + // Session-Stats setzen + $sessionStats = { + totalTokensIn: activeSession.token_input, + totalTokensOut: activeSession.token_output, + totalCost: activeSession.cost_usd, + messageCount: activeSession.message_count, + }; + + // Nachrichten laden + const messages: DbMessage[] = await invoke('load_messages', { sessionId: activeSession.id }); + if (messages.length > 0) { + setMessagesFromDb(messages); + console.log(`💬 ${messages.length} Nachrichten geladen`); + } + } + } catch (err) { + console.warn('Aktive Session konnte nicht geladen werden:', err); + } }); onDestroy(async () => {