feat: Chat-Panel herausloesbar + Expand-Button Fix [appimage]
All checks were successful
Build AppImage / build (push) Successful in 6m3s

- Neues Fenster fuer Chat-Panel (auf zweiten Monitor ziehbar)
- Expand/Collapse nutzt jetzt $state Array statt Set (Svelte 5 Reaktivitaet)
- Kein Collapse waehrend Streaming (verhindert unklickbare Buttons)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-20 23:01:27 +02:00
parent e0734303af
commit 2b7dfa6e37
4 changed files with 93 additions and 7 deletions

View file

@ -0,0 +1,31 @@
// Claude Desktop — Chat-Fenster herauslösen
// Öffnet den Chat-Panel in einem separaten Fenster
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
#[tauri::command]
pub async fn chat_window_open(app: AppHandle) -> Result<(), String> {
if let Some(win) = app.get_webview_window("chat-detached") {
win.show().map_err(|e| e.to_string())?;
win.set_focus().map_err(|e| e.to_string())?;
return Ok(());
}
WebviewWindowBuilder::new(&app, "chat-detached", WebviewUrl::App("/chat-window".into()))
.title("Claude \u{2014} Chat")
.inner_size(800.0, 900.0)
.min_inner_size(500.0, 400.0)
.center()
.build()
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
pub async fn chat_window_close(app: AppHandle) -> Result<(), String> {
if let Some(win) = app.get_webview_window("chat-detached") {
win.close().map_err(|e| e.to_string())?;
}
Ok(())
}

View file

@ -23,6 +23,7 @@ mod session;
mod teaching; mod teaching;
mod update; mod update;
mod voice; mod voice;
mod chat_window;
/// Initialisiert die App /// Initialisiert die App
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
@ -145,6 +146,9 @@ pub fn run() {
teaching::presentation_close, teaching::presentation_close,
teaching::presentation_send_slide, teaching::presentation_send_slide,
teaching::presentation_clear, teaching::presentation_clear,
// Chat-Fenster (herauslösen)
chat_window::chat_window_open,
chat_window::chat_window_close,
// Auto-Update // Auto-Update
update::check_for_update, update::check_for_update,
update::download_update, update::download_update,

View file

@ -7,6 +7,9 @@
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import CommandPalette from './CommandPalette.svelte'; import CommandPalette from './CommandPalette.svelte';
// Props
let { detached = false }: { detached?: boolean } = $props();
// Input-Referenz für Focus-Shortcuts // Input-Referenz für Focus-Shortcuts
let inputTextarea: HTMLTextAreaElement; let inputTextarea: HTMLTextAreaElement;
@ -25,15 +28,14 @@
// Collapse: Nachrichten mit > 25 Zeilen werden eingeklappt // Collapse: Nachrichten mit > 25 Zeilen werden eingeklappt
const COLLAPSE_LINES = 25; const COLLAPSE_LINES = 25;
let expandedMessages = new Set<string>(); let expandedMessages = $state<string[]>([]);
function toggleExpand(msgId: string) { function toggleExpand(msgId: string) {
if (expandedMessages.has(msgId)) { if (expandedMessages.includes(msgId)) {
expandedMessages.delete(msgId); expandedMessages = expandedMessages.filter(id => id !== msgId);
} else { } else {
expandedMessages.add(msgId); expandedMessages = [...expandedMessages, msgId];
} }
expandedMessages = expandedMessages;
} }
function shouldCollapse(content: string): boolean { function shouldCollapse(content: string): boolean {
@ -819,6 +821,11 @@
<span class="token-count" class:warning={estimatedTokens > 20000} class:danger={estimatedTokens > TOKEN_WARNING_THRESHOLD}> <span class="token-count" class:warning={estimatedTokens > 20000} class:danger={estimatedTokens > TOKEN_WARNING_THRESHOLD}>
~{(estimatedTokens / 1000).toFixed(1)}k Token ~{(estimatedTokens / 1000).toFixed(1)}k Token
</span> </span>
{#if !detached}
<button class="detach-btn" onclick={() => invoke('chat_window_open')} title="Chat herausloesen">&#x29C9;</button>
{:else}
<button class="detach-btn" onclick={() => invoke('chat_window_close')} title="Zurueck ins Hauptfenster">&#x29C7;</button>
{/if}
</div> </div>
</div> </div>
@ -878,7 +885,7 @@
</div> </div>
{:else if message.role === 'assistant'} {:else if message.role === 'assistant'}
{#if message.content} {#if message.content}
{#if shouldCollapse(message.content) && !expandedMessages.has(message.id)} {#if shouldCollapse(message.content) && !expandedMessages.includes(message.id) && !($isProcessing && isLastAssistantMessage(index))}
<div class="msg-collapsed"> <div class="msg-collapsed">
{@html renderMarkdown(message.content.split('\n').slice(0, COLLAPSE_LINES).join('\n') + '\n...')} {@html renderMarkdown(message.content.split('\n').slice(0, COLLAPSE_LINES).join('\n') + '\n...')}
</div> </div>
@ -887,7 +894,7 @@
</button> </button>
{:else} {:else}
{@html renderMarkdown(message.content)} {@html renderMarkdown(message.content)}
{#if shouldCollapse(message.content)} {#if shouldCollapse(message.content) && !($isProcessing && isLastAssistantMessage(index))}
<button class="expand-btn" onclick={() => toggleExpand(message.id)}> <button class="expand-btn" onclick={() => toggleExpand(message.id)}>
▲ Einklappen ▲ Einklappen
</button> </button>
@ -1154,6 +1161,24 @@
color: var(--error); color: var(--error);
} }
.detach-btn {
padding: 0.2rem 0.45rem;
background: var(--bg-tertiary, #2a2a3e);
border: 1px solid var(--border, #333);
border-radius: var(--radius-sm, 4px);
color: var(--text-secondary, #aaa);
cursor: pointer;
font-size: 0.85rem;
line-height: 1;
transition: all 0.15s ease;
}
.detach-btn:hover {
background: var(--accent, #6c8aff);
color: white;
border-color: var(--accent, #6c8aff);
}
.chat-messages { .chat-messages {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;

View file

@ -0,0 +1,26 @@
<script lang="ts">
import ChatPanel from '$lib/components/ChatPanel.svelte';
import { onMount } from 'svelte';
onMount(() => {
document.documentElement.classList.add('detached-window');
});
</script>
<div class="detached-chat">
<ChatPanel detached={true} />
</div>
<style>
:global(html.detached-window) {
background: var(--bg-primary, #1a1a2e);
}
.detached-chat {
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
overflow: hidden;
}
</style>