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 role: String, // "user", "assistant", "system"
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub model: Option<String>,
|
pub model: Option<String>,
|
||||||
|
pub agent_id: Option<String>, // Agent der die Nachricht erzeugt hat
|
||||||
pub timestamp: String,
|
pub timestamp: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,10 +161,13 @@ impl Database {
|
||||||
role TEXT NOT NULL,
|
role TEXT NOT NULL,
|
||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
model TEXT,
|
model TEXT,
|
||||||
|
agent_id TEXT,
|
||||||
timestamp TEXT NOT NULL,
|
timestamp TEXT NOT NULL,
|
||||||
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
|
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
|
||||||
|
|
||||||
|
"
|
||||||
",
|
",
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -517,19 +521,51 @@ impl Database {
|
||||||
Ok(())
|
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 ============
|
// ============ Messages ============
|
||||||
|
|
||||||
/// Speichert eine Nachricht
|
/// Speichert eine Nachricht
|
||||||
pub fn save_message(&self, msg: &ChatMessage) -> SqlResult<()> {
|
pub fn save_message(&self, msg: &ChatMessage) -> SqlResult<()> {
|
||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
"INSERT OR REPLACE INTO messages (id, session_id, role, content, model, timestamp)
|
"INSERT OR REPLACE INTO messages (id, session_id, role, content, model, agent_id, timestamp)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||||
params![
|
params![
|
||||||
msg.id,
|
msg.id,
|
||||||
msg.session_id,
|
msg.session_id,
|
||||||
msg.role,
|
msg.role,
|
||||||
msg.content,
|
msg.content,
|
||||||
msg.model,
|
msg.model,
|
||||||
|
msg.agent_id,
|
||||||
msg.timestamp,
|
msg.timestamp,
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -539,7 +575,7 @@ impl Database {
|
||||||
/// Lädt alle Nachrichten einer Session
|
/// Lädt alle Nachrichten einer Session
|
||||||
pub fn load_messages(&self, session_id: &str) -> SqlResult<Vec<ChatMessage>> {
|
pub fn load_messages(&self, session_id: &str) -> SqlResult<Vec<ChatMessage>> {
|
||||||
let mut stmt = self.conn.prepare(
|
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"
|
FROM messages WHERE session_id = ?1 ORDER BY timestamp ASC"
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|
@ -550,7 +586,8 @@ impl Database {
|
||||||
role: row.get(2)?,
|
role: row.get(2)?,
|
||||||
content: row.get(3)?,
|
content: row.get(3)?,
|
||||||
model: row.get(4)?,
|
model: row.get(4)?,
|
||||||
timestamp: row.get(5)?,
|
agent_id: row.get(5)?,
|
||||||
|
timestamp: row.get(6)?,
|
||||||
})
|
})
|
||||||
})?.collect::<SqlResult<Vec<_>>>()?;
|
})?.collect::<SqlResult<Vec<_>>>()?;
|
||||||
|
|
||||||
|
|
@ -563,6 +600,83 @@ impl Database {
|
||||||
Ok(())
|
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 ============
|
// ============ Settings ============
|
||||||
|
|
||||||
/// Speichert eine Einstellung
|
/// Speichert eine Einstellung
|
||||||
|
|
@ -732,3 +846,22 @@ pub async fn clear_messages(app: AppHandle, session_id: String) -> Result<(), St
|
||||||
let db = state.lock().unwrap();
|
let db = state.lock().unwrap();
|
||||||
db.clear_messages(&session_id).map_err(|e| e.to_string())
|
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::save_message,
|
||||||
db::load_messages,
|
db::load_messages,
|
||||||
db::clear_messages,
|
db::clear_messages,
|
||||||
|
db::compact_session,
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let handle = app.handle().clone();
|
let handle = app.handle().clone();
|
||||||
|
|
|
||||||
|
|
@ -231,6 +231,7 @@ export interface DbMessage {
|
||||||
role: string;
|
role: string;
|
||||||
content: string;
|
content: string;
|
||||||
model: string | null;
|
model: string | null;
|
||||||
|
agent_id: string | null; // Agent der die Nachricht erzeugt hat
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,6 +243,7 @@ export function messageToDb(msg: Message, sessionId: string): DbMessage {
|
||||||
role: msg.role,
|
role: msg.role,
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
model: msg.model || null,
|
model: msg.model || null,
|
||||||
|
agent_id: msg.agentId || null,
|
||||||
timestamp: msg.timestamp.toISOString(),
|
timestamp: msg.timestamp.toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -253,6 +255,7 @@ export function dbToMessage(db: DbMessage): Message {
|
||||||
role: db.role as Message['role'],
|
role: db.role as Message['role'],
|
||||||
content: db.content,
|
content: db.content,
|
||||||
model: db.model || undefined,
|
model: db.model || undefined,
|
||||||
|
agentId: db.agent_id || undefined,
|
||||||
timestamp: new Date(db.timestamp),
|
timestamp: new Date(db.timestamp),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,25 @@
|
||||||
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 } from '$lib/stores';
|
import { isProcessing, agentCount, currentModel, sessionStats, initEventListeners, cleanupEventListeners, currentSessionId, setMessagesFromDb, type DbMessage } from '$lib/stores';
|
||||||
import StopButton from '$lib/components/StopButton.svelte';
|
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 () => {
|
onMount(async () => {
|
||||||
await initEventListeners();
|
await initEventListeners();
|
||||||
|
|
||||||
|
|
@ -17,6 +33,32 @@
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Modell konnte nicht geladen werden:', 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 () => {
|
onDestroy(async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue