claude-desktop/src/lib/components/SessionList.svelte
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

363 lines
8 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import { invoke } from '@tauri-apps/api/core';
import { messages, clearAll, isProcessing, currentSessionId, setMessagesFromDb, type DbMessage } from '$lib/stores/app';
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;
}
let sessions: Session[] = [];
let activeSessionId: string | null = null;
let loading = true;
let showNewForm = false;
let newTitle = '';
async function loadSessions() {
try {
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();
});
async function createSession() {
const title = newTitle.trim() || `Session ${new Date().toLocaleDateString('de-DE')}`;
try {
const session: Session = await invoke('create_session', {
title,
workingDir: null,
});
activeSessionId = session.id;
$currentSessionId = session.id;
clearAll();
newTitle = '';
showNewForm = false;
await loadSessions();
} catch (err) {
console.error('Fehler:', err);
}
}
async function resumeSession(id: string) {
if (id === activeSessionId) return;
try {
const session: Session = await invoke('resume_session', { id });
activeSessionId = session.id;
$currentSessionId = session.id;
clearAll();
// Nachrichten aus DB laden
await loadSessionMessages(session.id);
} catch (err) {
console.error('Fehler:', err);
}
}
async function deleteSession(id: string) {
try {
await invoke('delete_session', { id });
if (activeSessionId === id) {
activeSessionId = null;
clearAll();
}
await loadSessions();
} catch (err) {
console.error('Fehler:', err);
}
}
function formatDate(dateStr: string): string {
const date = new Date(dateStr);
const now = new Date();
const diff = now.getTime() - date.getTime();
if (diff < 60000) return 'gerade eben';
if (diff < 3600000) return `vor ${Math.floor(diff / 60000)} Min`;
if (diff < 86400000) return `vor ${Math.floor(diff / 3600000)} Std`;
if (diff < 172800000) return 'gestern';
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
}
function formatCost(usd: number): string {
if (usd === 0) return '';
return `$${usd.toFixed(3)}`;
}
</script>
<div class="session-list">
<div class="session-header">
<h2>💬 Sessions</h2>
<button class="btn-new" on:click={() => showNewForm = !showNewForm}>
{showNewForm ? '✕' : '+ Neu'}
</button>
</div>
{#if showNewForm}
<div class="new-form">
<input
type="text"
bind:value={newTitle}
placeholder="Session-Titel (optional)"
on:keydown={(e) => e.key === 'Enter' && createSession()}
/>
<button class="btn-create" on:click={createSession}>Erstellen</button>
</div>
{/if}
{#if loading}
<div class="loading">Lade Sessions...</div>
{:else if sessions.length === 0}
<div class="empty">
<p>Keine Sessions vorhanden.</p>
<button class="btn-first" on:click={() => { showNewForm = true; }}>
Erste Session starten
</button>
</div>
{:else}
<div class="sessions">
{#each sessions as session}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="session-item"
class:active={session.id === activeSessionId}
on:click={() => resumeSession(session.id)}
>
<div class="session-main">
<span class="session-status">
{#if session.id === activeSessionId}
🟢
{:else if session.claude_session_id}
⏸️
{:else}
{/if}
</span>
<div class="session-info">
<span class="session-title">{session.title}</span>
<span class="session-meta">
{session.message_count} Nachrichten
{#if session.cost_usd > 0}
· {formatCost(session.cost_usd)}
{/if}
</span>
</div>
</div>
<div class="session-right">
<span class="session-time">{formatDate(session.updated_at)}</span>
<button
class="btn-delete"
on:click|stopPropagation={() => deleteSession(session.id)}
title="Session löschen"
>
🗑️
</button>
</div>
</div>
{/each}
</div>
{/if}
</div>
<style>
.session-list {
display: flex;
flex-direction: column;
height: 100%;
border-right: 1px solid var(--border);
background: var(--bg-primary);
}
.session-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(--border);
}
.session-header h2 {
font-size: 0.85rem;
font-weight: 600;
}
.btn-new {
padding: var(--spacing-xs) var(--spacing-sm);
background: var(--accent);
color: white;
border-radius: var(--radius-sm);
font-size: 0.7rem;
font-weight: 600;
}
.btn-new:hover {
background: var(--accent-hover);
}
/* Neues Session Formular */
.new-form {
display: flex;
gap: var(--spacing-xs);
padding: var(--spacing-sm);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
}
.new-form input {
flex: 1;
font-size: 0.75rem;
padding: var(--spacing-xs) var(--spacing-sm);
}
.btn-create {
padding: var(--spacing-xs) var(--spacing-sm);
background: var(--success);
color: white;
border-radius: var(--radius-sm);
font-size: 0.7rem;
font-weight: 600;
}
.loading, .empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--text-secondary);
font-size: 0.8rem;
padding: var(--spacing-md);
text-align: center;
}
.btn-first {
margin-top: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
background: var(--accent);
color: white;
border-radius: var(--radius-md);
font-size: 0.8rem;
}
/* Session-Items */
.sessions {
flex: 1;
overflow-y: auto;
}
.session-item {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-sm) var(--spacing-md);
text-align: left;
border-bottom: 1px solid var(--bg-tertiary);
transition: background 0.15s ease;
cursor: pointer;
}
.session-item:hover {
background: var(--bg-hover);
}
.session-item.active {
background: var(--bg-tertiary);
border-left: 3px solid var(--accent);
}
.session-main {
display: flex;
align-items: center;
gap: var(--spacing-sm);
flex: 1;
min-width: 0;
}
.session-status {
font-size: 0.6rem;
flex-shrink: 0;
}
.session-info {
display: flex;
flex-direction: column;
min-width: 0;
}
.session-title {
font-size: 0.8rem;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.session-meta {
font-size: 0.65rem;
color: var(--text-secondary);
}
.session-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
flex-shrink: 0;
}
.session-time {
font-size: 0.6rem;
color: var(--text-secondary);
}
.btn-delete {
font-size: 0.65rem;
opacity: 0;
padding: 2px;
transition: opacity 0.15s;
}
.session-item:hover .btn-delete {
opacity: 0.5;
}
.btn-delete:hover {
opacity: 1 !important;
}
</style>