From 8a7e0d87f3446bce23e023a127ca884d4cadd6d5 Mon Sep 17 00:00:00 2001 From: Eddy Date: Mon, 20 Apr 2026 22:31:25 +0200 Subject: [PATCH] feat: collapsible messages, German cost format, stats persistence [appimage] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Long messages (>25 lines) auto-collapse with expand/collapse button - Cost display uses German format: "16,23$" instead of "$16.230" - Session stats (tokens, cost, count) persist to DB after each response via new update_session_stats command — survives app restart - Small costs shown as cents (e.g. "3,2¢") Co-Authored-By: Claude Opus 4.6 --- src-tauri/src/lib.rs | 1 + src-tauri/src/session.rs | 27 +++++++++++++ src/lib/components/ChatPanel.svelte | 56 ++++++++++++++++++++++++++- src/lib/components/SessionList.svelte | 3 +- src/lib/stores/events.ts | 13 +++++++ src/routes/+layout.svelte | 6 +-- 6 files changed, 101 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index eb0c4e2..75a5a37 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -77,6 +77,7 @@ pub fn run() { session::resume_session, session::get_active_session, session::set_claude_session_id, + session::update_session_stats, // Messages db::save_message, db::load_messages, diff --git a/src-tauri/src/session.rs b/src-tauri/src/session.rs index dfaa1ce..9f991ed 100644 --- a/src-tauri/src/session.rs +++ b/src-tauri/src/session.rs @@ -153,3 +153,30 @@ pub async fn set_claude_session_id( Ok(()) } + +/// Aktualisiert nur die Statistiken einer Session (Token, Kosten, Message-Count) +#[tauri::command] +pub async fn update_session_stats( + app: AppHandle, + session_id: String, + token_input: i64, + token_output: i64, + cost_usd: f64, + message_count: i32, +) -> Result<(), String> { + let state = app.state::>>(); + let db = state.lock().unwrap(); + db.conn.execute( + "UPDATE sessions SET token_input = ?1, token_output = ?2, cost_usd = ?3, + message_count = ?4, updated_at = ?5 WHERE id = ?6", + rusqlite::params![ + token_input, + token_output, + cost_usd, + message_count, + chrono::Utc::now().to_rfc3339(), + session_id, + ], + ).map_err(|e| e.to_string())?; + Ok(()) +} diff --git a/src/lib/components/ChatPanel.svelte b/src/lib/components/ChatPanel.svelte index f2ead0a..edbc62d 100644 --- a/src/lib/components/ChatPanel.svelte +++ b/src/lib/components/ChatPanel.svelte @@ -23,6 +23,23 @@ marked.setOptions({ breaks: true, gfm: true, renderer }); + // Collapse: Nachrichten mit > 25 Zeilen werden eingeklappt + const COLLAPSE_LINES = 25; + let expandedMessages = new Set(); + + function toggleExpand(msgId: string) { + if (expandedMessages.has(msgId)) { + expandedMessages.delete(msgId); + } else { + expandedMessages.add(msgId); + } + expandedMessages = expandedMessages; + } + + function shouldCollapse(content: string): boolean { + return content.split('\n').length > COLLAPSE_LINES; + } + function renderMarkdown(text: string): string { try { return marked.parse(text) as string; @@ -831,7 +848,21 @@ {:else if message.role === 'assistant'} {#if message.content} - {@html renderMarkdown(message.content)} + {#if shouldCollapse(message.content) && !expandedMessages.has(message.id)} +
+ {@html renderMarkdown(message.content.split('\n').slice(0, COLLAPSE_LINES).join('\n') + '\n...')} +
+ + {:else} + {@html renderMarkdown(message.content)} + {#if shouldCollapse(message.content)} + + {/if} + {/if} {:else if $isProcessing} @@ -1270,6 +1301,29 @@ line-height: 1.6; } + .msg-collapsed { + overflow: hidden; + } + + .expand-btn { + display: block; + width: 100%; + padding: 0.35rem; + margin-top: 0.3rem; + background: var(--bg-tertiary, #2a2a2a); + border: 1px solid var(--border, #3a3a3a); + border-radius: 4px; + color: var(--accent, #6aa3ff); + font-size: 0.75rem; + cursor: pointer; + text-align: center; + transition: background 0.15s; + } + + .expand-btn:hover { + background: var(--bg-hover, #333); + } + /* Markdown-Styles innerhalb von Nachrichten */ .message-content :global(p) { margin: 0.3em 0; diff --git a/src/lib/components/SessionList.svelte b/src/lib/components/SessionList.svelte index 2e6b3b5..102f943 100644 --- a/src/lib/components/SessionList.svelte +++ b/src/lib/components/SessionList.svelte @@ -126,7 +126,8 @@ function formatCost(usd: number): string { if (usd === 0) return ''; - return `$${usd.toFixed(3)}`; + if (usd < 0.01) return `${(usd * 100).toFixed(1)}¢`; + return `${usd.toFixed(2).replace('.', ',')}$`; } diff --git a/src/lib/stores/events.ts b/src/lib/stores/events.ts index 3277b58..b33baa1 100644 --- a/src/lib/stores/events.ts +++ b/src/lib/stores/events.ts @@ -381,6 +381,19 @@ export async function initEventListeners(): Promise { outputTokens: tokens.output || 0, })); } + + // Session-Stats in DB persistieren (überlebt App-Neustart) + const appSessionId = get(currentSessionId); + if (appSessionId) { + const stats = get(sessionStats); + invoke('update_session_stats', { + sessionId: appSessionId, + tokenInput: stats.totalTokensIn, + tokenOutput: stats.totalTokensOut, + costUsd: stats.totalCost, + messageCount: stats.messageCount, + }).catch((err: unknown) => console.warn('Session-Stats speichern fehlgeschlagen:', err)); + } } }) ); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 2dd6eb3..bbf27c3 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -136,9 +136,9 @@ } function formatCost(usd: number): string { - if (usd === 0) return '$0'; - if (usd < 0.01) return `$${usd.toFixed(4)}`; - return `$${usd.toFixed(2)}`; + if (usd === 0) return '0$'; + if (usd < 0.01) return `${(usd * 100).toFixed(1)}¢`; + return `${usd.toFixed(2).replace('.', ',')}$`; } function formatTokens(n: number): string {