feat: Chat-Detach richtig — Hauptfenster blendet Chat aus, separates Fenster lädt Session
Some checks failed
Build AppImage / build (push) Failing after 15s

- chatDetached Store: Hauptfenster blendet Chat-Pane aus wenn Fenster offen
- Placeholder mit "Zurückholen"-Button statt leerem Pane
- Backend sendet chat-detached/chat-reattached Events ans Hauptfenster
- on_window_event(Destroyed): Automatisches Reattach wenn Fenster geschlossen
- Chat-Window lädt aktive Session + Nachrichten aus DB beim Öffnen
- Mehr Platz für andere Panels wenn Chat herausgelöst

[appimage]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-21 09:41:38 +02:00
parent 51374720e7
commit e87ac9ffc1
4 changed files with 121 additions and 7 deletions

View file

@ -1,17 +1,22 @@
// Claude Desktop — Chat-Fenster herauslösen // Claude Desktop — Chat-Fenster herauslösen
// Öffnet den Chat-Panel in einem separaten Fenster // Öffnet den Chat-Panel in einem separaten Fenster.
// Sendet Events an das Hauptfenster damit es den Chat-Bereich aus-/einblendet.
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder}; use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder};
#[tauri::command] #[tauri::command]
pub async fn chat_window_open(app: AppHandle) -> Result<(), String> { pub async fn chat_window_open(app: AppHandle) -> Result<(), String> {
// Falls Fenster schon existiert: anzeigen und fokussieren
if let Some(win) = app.get_webview_window("chat-detached") { if let Some(win) = app.get_webview_window("chat-detached") {
win.show().map_err(|e| e.to_string())?; win.show().map_err(|e| e.to_string())?;
win.set_focus().map_err(|e| e.to_string())?; win.set_focus().map_err(|e| e.to_string())?;
// Event ans Hauptfenster: Chat ist herausgelöst
app.emit("chat-detached", ()).ok();
return Ok(()); return Ok(());
} }
WebviewWindowBuilder::new(&app, "chat-detached", WebviewUrl::App("/chat-window".into())) // Neues Fenster erstellen
let win = WebviewWindowBuilder::new(&app, "chat-detached", WebviewUrl::App("/chat-window".into()))
.title("Claude \u{2014} Chat") .title("Claude \u{2014} Chat")
.inner_size(800.0, 900.0) .inner_size(800.0, 900.0)
.min_inner_size(500.0, 400.0) .min_inner_size(500.0, 400.0)
@ -19,6 +24,17 @@ pub async fn chat_window_open(app: AppHandle) -> Result<(), String> {
.build() .build()
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
// Event ans Hauptfenster: Chat ist herausgelöst
app.emit("chat-detached", ()).ok();
// Wenn das Chat-Fenster geschlossen wird → Hauptfenster benachrichtigen
let app_clone = app.clone();
win.on_window_event(move |event| {
if let tauri::WindowEvent::Destroyed = event {
app_clone.emit("chat-reattached", ()).ok();
}
});
Ok(()) Ok(())
} }
@ -27,5 +43,8 @@ pub async fn chat_window_close(app: AppHandle) -> Result<(), String> {
if let Some(win) = app.get_webview_window("chat-detached") { if let Some(win) = app.get_webview_window("chat-detached") {
win.close().map_err(|e| e.to_string())?; win.close().map_err(|e| e.to_string())?;
} }
// Event wird automatisch durch on_window_event/Destroyed gesendet
// Zur Sicherheit aber auch manuell senden
app.emit("chat-reattached", ()).ok();
Ok(()) Ok(())
} }

View file

@ -54,6 +54,7 @@ export const permissions = writable<Permission[]>([]);
// UI-State // UI-State
export const isProcessing = writable(false); export const isProcessing = writable(false);
export const chatDetached = writable(false);
export const currentInput = writable(''); export const currentInput = writable('');
export const selectedAgentId = writable<string | null>(null); export const selectedAgentId = writable<string | null>(null);
export const currentModel = writable(''); export const currentModel = writable('');

View file

@ -1,9 +1,25 @@
<script lang="ts"> <script lang="ts">
import { PaneGroup, Pane, PaneResizer } from 'paneforge'; import { PaneGroup, Pane, PaneResizer } from 'paneforge';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { onMount } from 'svelte';
import { chatDetached } from '$lib/stores/app';
// Kritische Panels sofort laden (immer sichtbar) // Kritische Panels sofort laden (immer sichtbar)
import SessionList from '$lib/components/SessionList.svelte'; import SessionList from '$lib/components/SessionList.svelte';
import ChatPanel from '$lib/components/ChatPanel.svelte'; import ChatPanel from '$lib/components/ChatPanel.svelte';
// Chat-Detach: Listener für Fenster-Events
onMount(async () => {
// Wenn das Chat-Fenster geschlossen wird → Chat wieder einblenden
await listen('chat-reattached', () => {
$chatDetached = false;
});
// Wenn das Chat-Fenster geöffnet wird → Chat ausblenden
await listen('chat-detached', () => {
$chatDetached = true;
});
});
// Sekundäre Panels: Lazy-Load bei erstem Tab-Wechsel // Sekundäre Panels: Lazy-Load bei erstem Tab-Wechsel
const lazyPanels = { const lazyPanels = {
activity: () => import('$lib/components/ActivityPanel.svelte'), activity: () => import('$lib/components/ActivityPanel.svelte'),
@ -69,9 +85,21 @@
</PaneResizer> </PaneResizer>
<!-- Chat --> <!-- Chat -->
<Pane defaultSize={35} minSize={15} class="panel"> {#if !$chatDetached}
<ChatPanel /> <Pane defaultSize={35} minSize={15} class="panel">
</Pane> <ChatPanel />
</Pane>
{:else}
<Pane defaultSize={12} minSize={5} maxSize={20} class="panel">
<div class="detached-placeholder">
<span class="detached-icon">💬</span>
<p>Chat ist herausgelöst</p>
<button class="reattach-btn" onclick={() => invoke('chat_window_close')}>
Zurückholen
</button>
</div>
</Pane>
{/if}
<PaneResizer class="resizer"> <PaneResizer class="resizer">
<div class="resizer-line"></div> <div class="resizer-line"></div>
@ -204,4 +232,44 @@
flex: 1; flex: 1;
overflow: auto; overflow: auto;
} }
/* Detached-Chat Placeholder */
.detached-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 8px;
color: var(--text-secondary);
}
.detached-icon {
font-size: 2rem;
opacity: 0.4;
}
.detached-placeholder p {
font-size: 0.8rem;
margin: 0;
opacity: 0.6;
}
.reattach-btn {
margin-top: 8px;
padding: 4px 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-secondary);
font-size: 0.75rem;
cursor: pointer;
transition: all 0.15s;
}
.reattach-btn:hover {
background: var(--accent);
color: white;
border-color: var(--accent);
}
</style> </style>

View file

@ -1,9 +1,35 @@
<script lang="ts"> <script lang="ts">
import ChatPanel from '$lib/components/ChatPanel.svelte'; import ChatPanel from '$lib/components/ChatPanel.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { invoke } from '@tauri-apps/api/core';
import { currentSessionId, setMessagesFromDb, type DbMessage } from '$lib/stores/app';
onMount(() => { onMount(async () => {
document.documentElement.classList.add('detached-window'); document.documentElement.classList.add('detached-window');
// Aktive Session und Nachrichten laden (aus DB, nicht aus Store — der ist leer)
try {
interface Session {
id: string;
title: string;
message_count: number;
token_input: number;
token_output: number;
cost_usd: number;
}
const session: Session | null = await invoke('get_active_session');
if (session) {
$currentSessionId = session.id;
const messages: DbMessage[] = await invoke('load_messages', { sessionId: session.id });
if (messages.length > 0) {
setMessagesFromDb(messages);
console.log(`💬 Chat-Window: ${messages.length} Nachrichten geladen`);
}
}
} catch (err) {
console.warn('Chat-Window: Session laden fehlgeschlagen:', err);
}
}); });
</script> </script>