Phase 6: Session-Management Verbesserungen
- Session Auto-Load bei App-Start (aktive Session + Nachrichten) - agent_id Spalte in messages-Tabelle für Agent-Zuordnung - DbMessage Interface erweitert (agent_id) - Session-Compacting: compact_session() fasst alte Nachrichten zusammen - Standard: 30 letzte Nachrichten behalten, Rest als Summary Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0e1cfe1b67
commit
abaf4eb9bf
4 changed files with 184 additions and 5 deletions
|
|
@ -35,6 +35,7 @@ pub struct ChatMessage {
|
|||
pub role: String, // "user", "assistant", "system"
|
||||
pub content: String,
|
||||
pub model: Option<String>,
|
||||
pub agent_id: Option<String>, // 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<Option<Session>> {
|
||||
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<Vec<ChatMessage>> {
|
||||
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::<SqlResult<Vec<_>>>()?;
|
||||
|
||||
|
|
@ -563,6 +600,83 @@ impl Database {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Zählt Nachrichten einer Session
|
||||
pub fn count_messages(&self, session_id: &str) -> SqlResult<usize> {
|
||||
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<usize> {
|
||||
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::<SqlResult<Vec<_>>>()?;
|
||||
|
||||
if old_messages.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
// Summary erstellen
|
||||
let mut summary_parts: Vec<String> = 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::<Vec<_>>().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<usize>,
|
||||
) -> Result<usize, String> {
|
||||
let keep = keep_last.unwrap_or(30); // Standard: letzte 30 Nachrichten behalten
|
||||
let state = app.state::<DbState>();
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue