Session-Historie: Nachrichten werden persistent gespeichert
- Neue messages-Tabelle in SQLite für Chat-Nachrichten - save_message, load_messages, clear_messages Tauri-Commands - User-Nachrichten werden beim Senden sofort gespeichert - Assistant-Nachrichten werden nach Abschluss gespeichert - Beim Session-Wechsel werden Nachrichten aus DB geladen - currentSessionId Store für Session-Tracking Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
433e2de2b6
commit
4ba14a53e1
6 changed files with 250 additions and 12 deletions
|
|
@ -27,6 +27,17 @@ pub struct Session {
|
|||
pub last_message: Option<String>,
|
||||
}
|
||||
|
||||
/// Eine Chat-Nachricht
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ChatMessage {
|
||||
pub id: String,
|
||||
pub session_id: String,
|
||||
pub role: String, // "user", "assistant", "system"
|
||||
pub content: String,
|
||||
pub model: Option<String>,
|
||||
pub timestamp: String,
|
||||
}
|
||||
|
||||
/// Datenbank-Wrapper
|
||||
pub struct Database {
|
||||
conn: Connection,
|
||||
|
|
@ -141,6 +152,18 @@ impl Database {
|
|||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Chat-Nachrichten
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
model 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(())
|
||||
|
|
@ -488,10 +511,58 @@ impl Database {
|
|||
|
||||
/// Löscht eine Session
|
||||
pub fn delete_session(&self, id: &str) -> SqlResult<()> {
|
||||
// Erst Nachrichten löschen (wegen Foreign Key)
|
||||
self.conn.execute("DELETE FROM messages WHERE session_id = ?1", params![id])?;
|
||||
self.conn.execute("DELETE FROM sessions WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============ 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)",
|
||||
params![
|
||||
msg.id,
|
||||
msg.session_id,
|
||||
msg.role,
|
||||
msg.content,
|
||||
msg.model,
|
||||
msg.timestamp,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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
|
||||
FROM messages WHERE session_id = ?1 ORDER BY timestamp ASC"
|
||||
)?;
|
||||
|
||||
let messages = stmt.query_map(params![session_id], |row| {
|
||||
Ok(ChatMessage {
|
||||
id: row.get(0)?,
|
||||
session_id: row.get(1)?,
|
||||
role: row.get(2)?,
|
||||
content: row.get(3)?,
|
||||
model: row.get(4)?,
|
||||
timestamp: row.get(5)?,
|
||||
})
|
||||
})?.collect::<SqlResult<Vec<_>>>()?;
|
||||
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
/// Löscht alle Nachrichten einer Session
|
||||
pub fn clear_messages(&self, session_id: &str) -> SqlResult<()> {
|
||||
self.conn.execute("DELETE FROM messages WHERE session_id = ?1", params![session_id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============ Settings ============
|
||||
|
||||
/// Speichert eine Einstellung
|
||||
|
|
@ -637,3 +708,27 @@ pub async fn get_all_settings(app: AppHandle) -> Result<Vec<(String, String)>, S
|
|||
let db = state.lock().unwrap();
|
||||
db.get_all_settings().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Nachricht speichern
|
||||
#[tauri::command]
|
||||
pub async fn save_message(app: AppHandle, message: ChatMessage) -> Result<(), String> {
|
||||
let state = app.state::<DbState>();
|
||||
let db = state.lock().unwrap();
|
||||
db.save_message(&message).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Nachrichten einer Session laden
|
||||
#[tauri::command]
|
||||
pub async fn load_messages(app: AppHandle, session_id: String) -> Result<Vec<ChatMessage>, String> {
|
||||
let state = app.state::<DbState>();
|
||||
let db = state.lock().unwrap();
|
||||
db.load_messages(&session_id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Alle Nachrichten einer Session löschen
|
||||
#[tauri::command]
|
||||
pub async fn clear_messages(app: AppHandle, session_id: String) -> Result<(), String> {
|
||||
let state = app.state::<DbState>();
|
||||
let db = state.lock().unwrap();
|
||||
db.clear_messages(&session_id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,10 @@ pub fn run() {
|
|||
session::resume_session,
|
||||
session::get_active_session,
|
||||
session::set_claude_session_id,
|
||||
// Messages
|
||||
db::save_message,
|
||||
db::load_messages,
|
||||
db::clear_messages,
|
||||
])
|
||||
.setup(|app| {
|
||||
let handle = app.handle().clone();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { messages, currentInput, isProcessing, addMessage } from '$lib/stores/app';
|
||||
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, type Message } from '$lib/stores/app';
|
||||
import { marked } from 'marked';
|
||||
import { tick } from 'svelte';
|
||||
import { tick, onDestroy } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
|
||||
|
|
@ -25,11 +26,56 @@
|
|||
|
||||
$: if ($messages.length) scrollToBottom();
|
||||
|
||||
// Nachricht in DB speichern
|
||||
async function saveMessageToDb(msg: Message) {
|
||||
const sessionId = get(currentSessionId);
|
||||
if (!sessionId) return;
|
||||
|
||||
try {
|
||||
const dbMsg = messageToDb(msg, sessionId);
|
||||
await invoke('save_message', { message: dbMsg });
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Speichern der Nachricht:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Neue Nachrichten automatisch speichern
|
||||
let lastMessageCount = 0;
|
||||
const unsubscribe = messages.subscribe(async (msgs) => {
|
||||
if (msgs.length > lastMessageCount && lastMessageCount > 0) {
|
||||
// Neue Nachricht(en) hinzugefügt
|
||||
const newMessages = msgs.slice(lastMessageCount);
|
||||
for (const msg of newMessages) {
|
||||
// Nur speichern wenn Nachricht Content hat (nicht die leere Streaming-Nachricht)
|
||||
if (msg.content && msg.content.trim()) {
|
||||
await saveMessageToDb(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
lastMessageCount = msgs.length;
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
async function sendMessage() {
|
||||
const text = $currentInput.trim();
|
||||
if (!text || $isProcessing) return;
|
||||
|
||||
addMessage('user', text);
|
||||
// Nachricht hinzufügen (wird durch den Store-Subscriber gespeichert)
|
||||
const msgId = crypto.randomUUID();
|
||||
const msg: Message = {
|
||||
id: msgId,
|
||||
role: 'user',
|
||||
content: text,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
messages.update((msgs) => [...msgs, msg]);
|
||||
|
||||
// Sofort speichern (nicht auf Subscriber warten)
|
||||
await saveMessageToDb(msg);
|
||||
|
||||
$currentInput = '';
|
||||
$isProcessing = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { messages, clearAll, isProcessing } from '$lib/stores/app';
|
||||
import { messages, clearAll, isProcessing, currentSessionId, setMessagesFromDb, type DbMessage } from '$lib/stores/app';
|
||||
|
||||
interface Session {
|
||||
id: string;
|
||||
|
|
@ -29,12 +29,28 @@
|
|||
sessions = await invoke('list_sessions', { limit: 50 });
|
||||
const active: Session | null = await invoke('get_active_session');
|
||||
activeSessionId = active?.id || null;
|
||||
$currentSessionId = activeSessionId;
|
||||
|
||||
// Wenn aktive Session existiert, Nachrichten laden
|
||||
if (activeSessionId) {
|
||||
await loadSessionMessages(activeSessionId);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden der Sessions:', err);
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
|
||||
async function loadSessionMessages(sessionId: string) {
|
||||
try {
|
||||
const dbMessages: DbMessage[] = await invoke('load_messages', { sessionId });
|
||||
setMessagesFromDb(dbMessages);
|
||||
console.log(`📨 ${dbMessages.length} Nachrichten geladen`);
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden der Nachrichten:', err);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadSessions();
|
||||
});
|
||||
|
|
@ -47,6 +63,7 @@
|
|||
workingDir: null,
|
||||
});
|
||||
activeSessionId = session.id;
|
||||
$currentSessionId = session.id;
|
||||
clearAll();
|
||||
newTitle = '';
|
||||
showNewForm = false;
|
||||
|
|
@ -61,8 +78,10 @@
|
|||
try {
|
||||
const session: Session = await invoke('resume_session', { id });
|
||||
activeSessionId = session.id;
|
||||
$currentSessionId = session.id;
|
||||
clearAll();
|
||||
// TODO: Nachrichten aus Session-Historie laden
|
||||
// Nachrichten aus DB laden
|
||||
await loadSessionMessages(session.id);
|
||||
} catch (err) {
|
||||
console.error('Fehler:', err);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export const isProcessing = writable(false);
|
|||
export const currentInput = writable('');
|
||||
export const selectedAgentId = writable<string | null>(null);
|
||||
export const currentModel = writable('');
|
||||
export const currentSessionId = writable<string | null>(null);
|
||||
|
||||
// Session-Statistiken (kumuliert)
|
||||
export const sessionStats = writable({
|
||||
|
|
@ -156,3 +157,41 @@ export function clearAll() {
|
|||
messages.set([]);
|
||||
isProcessing.set(false);
|
||||
}
|
||||
|
||||
// DB-Nachricht Format (für Tauri)
|
||||
export interface DbMessage {
|
||||
id: string;
|
||||
session_id: string;
|
||||
role: string;
|
||||
content: string;
|
||||
model: string | null;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
// Konvertierung: Store → DB
|
||||
export function messageToDb(msg: Message, sessionId: string): DbMessage {
|
||||
return {
|
||||
id: msg.id,
|
||||
session_id: sessionId,
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
model: msg.model || null,
|
||||
timestamp: msg.timestamp.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// Konvertierung: DB → Store
|
||||
export function dbToMessage(db: DbMessage): Message {
|
||||
return {
|
||||
id: db.id,
|
||||
role: db.role as Message['role'],
|
||||
content: db.content,
|
||||
model: db.model || undefined,
|
||||
timestamp: new Date(db.timestamp),
|
||||
};
|
||||
}
|
||||
|
||||
// Nachrichten aus DB in Store laden
|
||||
export function setMessagesFromDb(dbMessages: DbMessage[]) {
|
||||
messages.set(dbMessages.map(dbToMessage));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
// Empfängt Events vom Tauri-Backend und aktualisiert die Stores
|
||||
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { get } from 'svelte/store';
|
||||
import {
|
||||
agents,
|
||||
toolCalls,
|
||||
|
|
@ -14,7 +16,10 @@ import {
|
|||
completeToolCall,
|
||||
clearAll,
|
||||
currentModel,
|
||||
sessionStats
|
||||
sessionStats,
|
||||
currentSessionId,
|
||||
messageToDb,
|
||||
type Message
|
||||
} from './app';
|
||||
|
||||
// Event-Typen vom Backend
|
||||
|
|
@ -53,6 +58,20 @@ let listeners: UnlistenFn[] = [];
|
|||
// Streaming: ID der aktuellen Live-Nachricht
|
||||
let streamingMessageId: string | null = null;
|
||||
|
||||
// Nachricht in DB speichern
|
||||
async function saveMessageToDb(msg: Message) {
|
||||
const sessionId = get(currentSessionId);
|
||||
if (!sessionId) return;
|
||||
|
||||
try {
|
||||
const dbMsg = messageToDb(msg, sessionId);
|
||||
await invoke('save_message', { message: dbMsg });
|
||||
console.log('💾 Nachricht gespeichert:', msg.role);
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Speichern der Nachricht:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Events initialisieren
|
||||
export async function initEventListeners(): Promise<void> {
|
||||
console.log('🎧 Initialisiere Event-Listener...');
|
||||
|
|
@ -161,15 +180,31 @@ export async function initEventListeners(): Promise<void> {
|
|||
|
||||
// Ergebnis (Kosten, Token, Modell)
|
||||
listeners.push(
|
||||
await listen<ResultEvent>('claude-result', (event) => {
|
||||
await listen<ResultEvent>('claude-result', async (event) => {
|
||||
const { cost, tokens, session_id, model } = event.payload;
|
||||
console.log('📊 Ergebnis:', { cost: cost ? `$${cost.toFixed(4)}` : '-', tokens, model });
|
||||
|
||||
// Modell an die Streaming-Nachricht anhängen
|
||||
if (model && streamingMessageId) {
|
||||
messages.update((msgs) =>
|
||||
msgs.map((m) => m.id === streamingMessageId ? { ...m, model } : m)
|
||||
);
|
||||
// Modell an die Streaming-Nachricht anhängen und speichern
|
||||
if (streamingMessageId) {
|
||||
let finalMessage: Message | null = null;
|
||||
|
||||
messages.update((msgs) => {
|
||||
return msgs.map((m) => {
|
||||
if (m.id === streamingMessageId) {
|
||||
finalMessage = { ...m, model: model || m.model };
|
||||
return finalMessage;
|
||||
}
|
||||
return m;
|
||||
});
|
||||
});
|
||||
|
||||
// Nachricht in DB speichern (nur wenn Content vorhanden)
|
||||
if (finalMessage && finalMessage.content && finalMessage.content.trim()) {
|
||||
await saveMessageToDb(finalMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (model) {
|
||||
currentModel.set(model);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue