Compare commits

..

2 commits

Author SHA1 Message Date
Eddy
4ba14a53e1 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>
2026-04-14 10:35:04 +02:00
Eddy
433e2de2b6 Modell-Auswahl in Settings implementiert
- Neues SettingsPanel mit Modell-Auswahl (Haiku/Sonnet/Opus)
- Modell wird in SQLite persistiert (claude_model Setting)
- Bridge unterstützt set-model und get-models Commands
- Modell kann zur Laufzeit gewechselt werden
- Preisanzeige pro Modell im Settings-Panel
- Aktuelles Modell wird beim App-Start geladen

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-14 09:32:26 +02:00
11 changed files with 709 additions and 25 deletions

View file

@ -17,7 +17,14 @@ process.stdin.resume();
let activeAbort = null;
let currentAgentId = null;
const MODEL = process.env.CLAUDE_MODEL || 'opus';
let currentModel = process.env.CLAUDE_MODEL || 'opus';
// Verfügbare Modelle
const AVAILABLE_MODELS = [
{ id: 'haiku', name: 'Claude Haiku', description: 'Schnell & günstig' },
{ id: 'sonnet', name: 'Claude Sonnet', description: 'Ausgewogen' },
{ id: 'opus', name: 'Claude Opus', description: 'Leistungsstark' },
];
// ============ Kommunikation mit Tauri ============
@ -39,7 +46,10 @@ function sendError(id, error) {
// ============ Claude Agent SDK ============
async function sendMessage(message, requestId) {
async function sendMessage(message, requestId, model = null) {
// Modell für diese Anfrage (Parameter > State > Default)
const useModel = model || currentModel;
currentAgentId = randomUUID();
activeAbort = new AbortController();
@ -47,19 +57,20 @@ async function sendMessage(message, requestId) {
id: currentAgentId,
type: 'Main',
task: message.substring(0, 100),
model: useModel,
});
sendResponse(requestId, { agentId: currentAgentId, status: 'gestartet' });
sendResponse(requestId, { agentId: currentAgentId, status: 'gestartet', model: useModel });
const startTime = Date.now();
let fullText = '';
let usedModel = MODEL;
let usedModel = useModel;
try {
const conversation = query({
prompt: message,
options: {
model: MODEL,
model: useModel,
maxTurns: 25,
abortController: activeAbort,
},
@ -140,7 +151,8 @@ function handleCommand(msg) {
sendError(msg.id, 'Keine Nachricht angegeben');
return;
}
sendMessage(msg.message, msg.id);
// Modell kann pro Anfrage überschrieben werden
sendMessage(msg.message, msg.id, msg.model);
break;
case 'stop':
@ -150,10 +162,33 @@ function handleCommand(msg) {
sendResponse(msg.id, { status: 'gestoppt' });
break;
case 'set-model':
if (!msg.model) {
sendError(msg.id, 'Kein Modell angegeben');
return;
}
const validModels = AVAILABLE_MODELS.map(m => m.id);
if (!validModels.includes(msg.model)) {
sendError(msg.id, `Ungültiges Modell: ${msg.model}. Verfügbar: ${validModels.join(', ')}`);
return;
}
currentModel = msg.model;
sendResponse(msg.id, { model: currentModel, status: 'Modell geändert' });
sendEvent('model-changed', { model: currentModel });
break;
case 'get-models':
sendResponse(msg.id, {
current: currentModel,
available: AVAILABLE_MODELS,
});
break;
case 'status':
sendResponse(msg.id, {
model: MODEL,
model: currentModel,
isProcessing: !!currentAgentId,
availableModels: AVAILABLE_MODELS,
});
break;
@ -186,4 +221,4 @@ process.on('SIGTERM', () => { clearInterval(keepAlive); process.exit(0); });
process.on('SIGINT', () => { clearInterval(keepAlive); process.exit(0); });
// Bereit
sendEvent('ready', { version: '1.0.0', pid: process.pid, model: MODEL });
sendEvent('ready', { version: '1.1.0', pid: process.pid, model: currentModel, availableModels: AVAILABLE_MODELS });

View file

@ -264,11 +264,19 @@ fn send_to_bridge(app: &AppHandle, command: &str, message: &str) -> Result<Strin
state.request_counter += 1;
let request_id = format!("req-{}", state.request_counter);
let msg = serde_json::json!({
// Je nach Command unterschiedliche Payload-Struktur
let msg = match command {
"set-model" => serde_json::json!({
"command": command,
"id": request_id,
"model": message
}),
_ => serde_json::json!({
"command": command,
"id": request_id,
"message": message
});
}),
};
if let Some(stdin) = &mut state.bridge_stdin {
writeln!(stdin, "{}", msg.to_string()).map_err(|e| e.to_string())?;
@ -323,3 +331,75 @@ pub async fn get_agent_status(app: AppHandle) -> Result<Vec<AgentStatus>, String
let state = state.lock().unwrap();
Ok(state.agents.clone())
}
/// Modell wechseln
#[tauri::command]
pub async fn set_model(app: AppHandle, model: String) -> Result<String, String> {
println!("🔄 Modell wechseln zu: {}", model);
// Modell in Settings speichern
if let Some(db_state) = app.try_state::<Arc<Mutex<crate::db::Database>>>() {
let db = db_state.lock().unwrap();
let _ = db.set_setting("claude_model", &model);
}
// 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)?;
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
}
// Modell an Bridge senden
send_to_bridge(&app, "set-model", &model)?;
Ok(model)
}
/// Verfügbare Modelle abrufen
#[tauri::command]
pub async fn get_available_models() -> Result<Vec<ModelInfo>, String> {
Ok(vec![
ModelInfo {
id: "haiku".to_string(),
name: "Claude Haiku".to_string(),
description: "Schnell & günstig".to_string(),
},
ModelInfo {
id: "sonnet".to_string(),
name: "Claude Sonnet".to_string(),
description: "Ausgewogen".to_string(),
},
ModelInfo {
id: "opus".to_string(),
name: "Claude Opus".to_string(),
description: "Leistungsstark".to_string(),
},
])
}
/// Aktuelles Modell aus Settings laden
#[tauri::command]
pub async fn get_current_model(app: AppHandle) -> Result<String, String> {
if let Some(db_state) = app.try_state::<Arc<Mutex<crate::db::Database>>>() {
let db = db_state.lock().unwrap();
if let Ok(Some(model)) = db.get_setting("claude_model") {
return Ok(model);
}
}
// Default
Ok("opus".to_string())
}
/// Modell-Info Struct
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ModelInfo {
pub id: String,
pub name: String,
pub description: String,
}

View file

@ -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
@ -517,6 +588,15 @@ impl Database {
}
}
/// Lädt alle Einstellungen
pub fn get_all_settings(&self) -> SqlResult<Vec<(String, String)>> {
let mut stmt = self.conn.prepare("SELECT key, value FROM settings")?;
let settings = stmt.query_map([], |row| {
Ok((row.get(0)?, row.get(1)?))
})?.collect::<SqlResult<Vec<_>>>()?;
Ok(settings)
}
// ============ Statistiken ============
/// DB-Statistiken
@ -604,3 +684,51 @@ pub async fn get_db_stats(app: AppHandle) -> Result<DbStats, String> {
let db = state.lock().unwrap();
db.stats().map_err(|e| e.to_string())
}
/// Einstellung lesen
#[tauri::command]
pub async fn get_setting(app: AppHandle, key: String) -> Result<Option<String>, String> {
let state = app.state::<DbState>();
let db = state.lock().unwrap();
db.get_setting(&key).map_err(|e| e.to_string())
}
/// Einstellung speichern
#[tauri::command]
pub async fn set_setting(app: AppHandle, key: String, value: String) -> Result<(), String> {
let state = app.state::<DbState>();
let db = state.lock().unwrap();
db.set_setting(&key, &value).map_err(|e| e.to_string())
}
/// Alle Einstellungen laden
#[tauri::command]
pub async fn get_all_settings(app: AppHandle) -> Result<Vec<(String, String)>, String> {
let state = app.state::<DbState>();
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())
}

View file

@ -23,6 +23,9 @@ pub fn run() {
claude::send_message,
claude::stop_all_agents,
claude::get_agent_status,
claude::set_model,
claude::get_available_models,
claude::get_current_model,
// Gedächtnis-System
memory::load_memory,
memory::get_sticky_context,
@ -41,6 +44,10 @@ pub fn run() {
// Datenbank
db::init_database,
db::get_db_stats,
// Settings
db::get_setting,
db::set_setting,
db::get_all_settings,
// Sessions
session::create_session,
session::update_session,
@ -50,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();

View file

@ -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;

View file

@ -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);
}

View file

@ -0,0 +1,277 @@
<script lang="ts">
import { onMount } from 'svelte';
import { invoke } from '@tauri-apps/api/core';
import { currentModel } from '$lib/stores/app';
interface ModelInfo {
id: string;
name: string;
description: string;
}
let availableModels: ModelInfo[] = [];
let selectedModel = '';
let loading = true;
let saving = false;
// Modell-Icons
const modelIcons: Record<string, string> = {
haiku: '🐦',
sonnet: '📝',
opus: '🎭',
};
// Preise pro 1M Token (ungefähre Werte)
const modelPrices: Record<string, { input: number; output: number }> = {
haiku: { input: 0.25, output: 1.25 },
sonnet: { input: 3, output: 15 },
opus: { input: 15, output: 75 },
};
async function loadSettings() {
try {
availableModels = await invoke('get_available_models');
const current: string = await invoke('get_current_model');
selectedModel = current;
$currentModel = current;
} catch (err) {
console.error('Fehler beim Laden:', err);
}
loading = false;
}
async function changeModel(modelId: string) {
if (modelId === selectedModel) return;
saving = true;
try {
await invoke('set_model', { model: modelId });
selectedModel = modelId;
$currentModel = modelId;
} catch (err) {
console.error('Fehler beim Speichern:', err);
}
saving = false;
}
onMount(() => {
loadSettings();
});
function formatPrice(price: number): string {
return `$${price.toFixed(2)}`;
}
</script>
<div class="settings-panel">
<div class="panel-header">
<h2>⚙️ Einstellungen</h2>
</div>
{#if loading}
<div class="loading">Lade Einstellungen...</div>
{:else}
<div class="settings-content">
<!-- Modell-Auswahl -->
<section class="settings-section">
<h3>🤖 Claude-Modell</h3>
<p class="section-hint">Wähle das Modell für deine Anfragen</p>
<div class="model-list">
{#each availableModels as model}
<button
class="model-card"
class:selected={selectedModel === model.id}
class:saving={saving && selectedModel !== model.id}
on:click={() => changeModel(model.id)}
disabled={saving}
>
<div class="model-header">
<span class="model-icon">{modelIcons[model.id] || '🤖'}</span>
<span class="model-name">{model.name}</span>
{#if selectedModel === model.id}
<span class="model-active">✓ Aktiv</span>
{/if}
</div>
<div class="model-description">{model.description}</div>
{#if modelPrices[model.id]}
<div class="model-pricing">
<span>Input: {formatPrice(modelPrices[model.id].input)}/1M</span>
<span>Output: {formatPrice(modelPrices[model.id].output)}/1M</span>
</div>
{/if}
</button>
{/each}
</div>
</section>
<!-- Weitere Einstellungen (Platzhalter) -->
<section class="settings-section">
<h3>🎨 Darstellung</h3>
<div class="setting-row">
<span class="setting-label">Theme</span>
<span class="setting-value">AWL Dark (fest)</span>
</div>
</section>
<section class="settings-section">
<h3>📁 Pfade</h3>
<div class="setting-row">
<span class="setting-label">Arbeitsverzeichnis</span>
<span class="setting-value placeholder">Wird aus Session geladen</span>
</div>
</section>
</div>
{/if}
</div>
<style>
.settings-panel {
display: flex;
flex-direction: column;
height: 100%;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--bg-secondary);
border-bottom: 1px solid var(--bg-tertiary);
}
.panel-header h2 {
font-size: 0.875rem;
font-weight: 600;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-secondary);
}
.settings-content {
flex: 1;
overflow-y: auto;
padding: var(--spacing-md);
}
.settings-section {
margin-bottom: var(--spacing-lg);
}
.settings-section h3 {
font-size: 0.875rem;
font-weight: 600;
margin-bottom: var(--spacing-xs);
}
.section-hint {
font-size: 0.75rem;
color: var(--text-secondary);
margin-bottom: var(--spacing-md);
}
/* Modell-Karten */
.model-list {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.model-card {
width: 100%;
text-align: left;
padding: var(--spacing-md);
background: var(--bg-secondary);
border: 2px solid var(--bg-tertiary);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
}
.model-card:hover:not(:disabled) {
background: var(--bg-tertiary);
border-color: var(--text-secondary);
}
.model-card.selected {
border-color: var(--accent);
background: rgba(233, 69, 96, 0.1);
}
.model-card.saving {
opacity: 0.5;
cursor: wait;
}
.model-card:disabled {
cursor: not-allowed;
}
.model-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-xs);
}
.model-icon {
font-size: 1.2rem;
}
.model-name {
font-weight: 600;
flex: 1;
}
.model-active {
font-size: 0.7rem;
padding: 2px 6px;
background: var(--accent);
color: white;
border-radius: var(--radius-sm);
}
.model-description {
font-size: 0.8rem;
color: var(--text-secondary);
margin-bottom: var(--spacing-xs);
}
.model-pricing {
display: flex;
gap: var(--spacing-md);
font-size: 0.7rem;
color: var(--text-secondary);
font-family: var(--font-mono);
}
/* Allgemeine Einstellungs-Zeilen */
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-sm) 0;
border-bottom: 1px solid var(--bg-tertiary);
}
.setting-label {
font-size: 0.85rem;
}
.setting-value {
font-size: 0.85rem;
color: var(--text-secondary);
}
.setting-value.placeholder {
font-style: italic;
opacity: 0.6;
}
</style>

View file

@ -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));
}

View file

@ -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);
}

View file

@ -7,6 +7,16 @@
onMount(async () => {
await initEventListeners();
// Aktuelles Modell aus Settings laden
try {
const model: string = await invoke('get_current_model');
if (model) {
$currentModel = model;
}
} catch (err) {
console.warn('Modell konnte nicht geladen werden:', err);
}
});
onDestroy(async () => {

View file

@ -7,6 +7,7 @@
import MemoryPanel from '$lib/components/MemoryPanel.svelte';
import AuditLog from '$lib/components/AuditLog.svelte';
import GuardRailsPanel from '$lib/components/GuardRailsPanel.svelte';
import SettingsPanel from '$lib/components/SettingsPanel.svelte';
let activeMiddleTab = 'activity';
let activeRightTab = 'agents';
@ -20,6 +21,7 @@
const rightTabs = [
{ id: 'agents', label: 'Agents', icon: '🤖' },
{ id: 'guards', label: 'Guard-Rails', icon: '🛡️' },
{ id: 'settings', label: 'Settings', icon: '⚙️' },
];
</script>
@ -88,6 +90,8 @@
<AgentView />
{:else if activeRightTab === 'guards'}
<GuardRailsPanel />
{:else if activeRightTab === 'settings'}
<SettingsPanel />
{/if}
</div>
</Pane>