claude-desktop/src/routes/+layout.svelte
Eddy ad9833fcb8
Some checks failed
Build AppImage / build (push) Has been cancelled
feat: Phase 8+9 — Inline Tool-Karten + komplettes UI-Redesign auf Cursor/Zed-Niveau [appimage]
Phase 8 (VS-Code-Look Chatbereich):
- Linksbuendige Messages mit Avatar-Spalte, Hover-Actions
- Inline Tool-Karten (Read/Edit/Bash/Generic) in Assistant-Messages
- Edit-Karten zeigen Diff direkt mit Accept/Reject
- Tool-Calls werden via events.ts an letzte Assistant-Message gebunden
- Smart-Sticky-Scroll (Auto-Scroll stoppt wenn User selbst scrollt)
- OOM-Bug durch MutationObserver mit subtree:true behoben

Phase 9 (Komplettes UI-Redesign):
- Design-System in app.css: 4 Graustufen, 1 Akzent (#007acc), 4 Status-Farben,
  5 Schriftgroessen (11/12/13/14/16), 4-Punkt-Spacing, 2 Radius-Werte
- vscode.css als Aliase auf das neue System
- UI-Library src/lib/ui/: Button, Card, Icon, Badge, StatusDot, Tooltip, Drawer, Tabs
- Lucide-svelte fuer SVG-Icons (ersetzt Emojis im Chrome)
- StatusBar (22px) ersetzt ueberfuellten Footer mit 6+ Stats
- Titlebar entruempelt: ✱-Logo + Stop + Schulungsmodus + Version
- 2-spaltiges Layout (Sidebar 240px + Hauptbereich) statt 4-Pane-Zerstueckelung
- ToolDrawer: 13 Panels in 4 Gruppen (Aktivitaet/Speicher/Werkzeuge/Einstellungen),
  jede Gruppe mit internen Tabs, Esc schliesst
- Cmd+K global oeffnet QuickActions als zentrale Navigation
- StatusDot-Komponente ersetzt Emoji-Status (🟢🟡🔴) in AgentView
- Hardgecodete Farben (#ef4444, #22c55e, #eab308 ...) in 9 Komponenten durch
  CSS-Variablen ersetzt

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:27:09 +02:00

273 lines
6.8 KiB
Svelte

<script lang="ts">
import '../app.css';
import '$lib/theme/vscode.css';
import { onMount, onDestroy } from 'svelte';
import { invoke } from '@tauri-apps/api/core';
import { isProcessing, agentCount, currentModel, sessionStats, contextPercent, contextUsage, initEventListeners, cleanupEventListeners, currentSessionId, setMessagesFromDb, stickyContextInfo, agentMode, queuedMessage, messageQueue, type DbMessage, type StickyContextInfo, type AgentMode } from '$lib/stores';
import StopButton from '$lib/components/StopButton.svelte';
import UpdateDialog from '$lib/components/UpdateDialog.svelte';
import StatusBar from '$lib/components/StatusBar.svelte';
import { GraduationCap } from 'lucide-svelte';
import Icon from '$lib/ui/Icon.svelte';
import Tooltip from '$lib/ui/Tooltip.svelte';
// Session-Typ vom Backend
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;
}
// Backend-Response für Sticky Context
interface StickyContextResponse {
loaded: boolean;
entries: number;
estimated_tokens: number;
has_user_info: boolean;
has_project: boolean;
credentials_count: number;
rules_count: number;
}
let appVersion = '';
onMount(async () => {
await initEventListeners();
// App-Version aus Rust holen (wird von der Pipeline als APP_VERSION gesetzt)
try {
appVersion = await invoke('get_current_version');
} catch (err) {
console.warn('App-Version konnte nicht geladen werden:', err);
appVersion = 'dev';
}
// 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);
}
// Agent-Modus aus Settings laden (sonst Badge nicht sofort sichtbar)
try {
const mode: string = await invoke('get_agent_mode');
if (mode) {
$agentMode = mode as AgentMode;
}
} catch (err) {
console.warn('Agent-Modus konnte nicht geladen werden:', err);
}
// Sticky Context beim Start laden und an Bridge senden
try {
const ctx: StickyContextResponse = await invoke('init_sticky_context');
$stickyContextInfo = {
loaded: ctx.loaded,
entries: ctx.entries,
estimatedTokens: ctx.estimated_tokens,
hasUserInfo: ctx.has_user_info,
hasProject: ctx.has_project,
credentialsCount: ctx.credentials_count,
rulesCount: ctx.rules_count,
};
if (ctx.loaded) {
console.log(`📌 Sticky Context geladen: ${ctx.entries} Einträge, ~${ctx.estimated_tokens} Token`);
}
} catch (err) {
console.warn('Sticky Context konnte nicht geladen werden:', err);
}
// Aktive Session automatisch laden (falls vorhanden)
try {
const activeSession: Session | null = await invoke('get_active_session');
if (activeSession) {
console.log('📂 Lade aktive Session:', activeSession.title);
$currentSessionId = activeSession.id;
// Session-Stats setzen
$sessionStats = {
totalTokensIn: activeSession.token_input,
totalTokensOut: activeSession.token_output,
totalCost: activeSession.cost_usd,
messageCount: activeSession.message_count,
};
// Nachrichten laden
const messages: DbMessage[] = await invoke('load_messages', { sessionId: activeSession.id });
if (messages.length > 0) {
setMessagesFromDb(messages);
console.log(`💬 ${messages.length} Nachrichten geladen`);
}
}
} catch (err) {
console.warn('Aktive Session konnte nicht geladen werden:', err);
}
});
onDestroy(async () => {
await cleanupEventListeners();
});
async function handleStop() {
try {
await invoke('stop_all_agents');
} catch (err) {
console.error('Fehler beim Stoppen:', err);
}
// Wartende Nachrichten verwerfen — der User hat bewusst abgebrochen,
// die Queue soll nicht automatisch nachfeuern.
$queuedMessage = null;
$messageQueue = [];
$isProcessing = false;
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape' && $isProcessing) {
handleStop();
}
}
function formatCost(usd: number): string {
if (usd === 0) return '0$';
if (usd < 0.01) return `${(usd * 100).toFixed(1)}¢`;
return `${usd.toFixed(2).replace('.', ',')}$`;
}
function formatTokens(n: number): string {
if (n === 0) return '0';
if (n < 1000) return String(n);
return `${(n / 1000).toFixed(1)}k`;
}
</script>
<svelte:window on:keydown={handleKeydown} />
<div class="app-container">
<!-- Titelleiste (Phase 9: minimal — nur Logo, Stop, Schulungsmodus, Version) -->
<header class="titlebar">
<div class="titlebar-left">
<span class="brand-mark"></span>
<span class="brand">Claude Desktop</span>
</div>
<div class="titlebar-right">
{#if $isProcessing}
<StopButton on:click={handleStop} />
{/if}
<Tooltip text="Schulungsmodus">
<button
class="icon-btn"
onclick={() => invoke('presentation_open').catch(e => console.error(e))}
>
<Icon icon={GraduationCap} size={16} />
</button>
</Tooltip>
{#if appVersion}
<span class="version-badge" class:dev={appVersion === 'dev'} title="App-Version">
v{appVersion}
</span>
{/if}
</div>
</header>
<main class="main-content">
<slot />
</main>
<!-- Status-Bar (Phase 9: ersetzt den alten Footer) -->
<StatusBar />
</div>
<!-- Auto-Update Dialog -->
<UpdateDialog />
<style>
.app-container {
display: flex;
flex-direction: column;
height: 100vh;
background: var(--bg-primary);
}
.titlebar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--sp-3);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
user-select: none;
height: var(--titlebar-height);
flex-shrink: 0;
}
.titlebar-left {
display: flex;
align-items: center;
gap: var(--sp-2);
}
.brand-mark {
color: var(--accent);
font-size: 14px;
font-weight: var(--fw-semi);
}
.brand {
font-size: var(--fs-md);
font-weight: var(--fw-semi);
color: var(--text-primary);
}
.titlebar-right {
display: flex;
align-items: center;
gap: var(--sp-2);
}
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: var(--r-sm);
color: var(--text-secondary);
background: transparent;
border: 0;
cursor: pointer;
transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.icon-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.version-badge {
font-size: var(--fs-xs);
color: var(--text-disabled);
font-family: var(--font-mono);
}
.version-badge.dev {
font-style: italic;
}
.main-content {
flex: 1;
overflow: hidden;
min-height: 0;
}
</style>