Bridge (claude-bridge.js):
- Resume-Fix: queryOptions.resume statt .sessionId (SDK-API)
- tools-Whitelist statt disallowedTools (Blacklist vererbt sich auf Sub-Agents!)
Handlanger: Main nur Task+TodoWrite, Sub-Agents bekommen volles Tool-Set
Experten: Main nur Task+TodoWrite+Read+Grep+Glob
Solo: preset claude_code
- handleToolUse/handleToolResult Helper, greifen auch in assistant.content-Bloecken
(SDK liefert tool_use/tool_result nicht als standalone events)
- Dedup via handledTools Set
- Resume-Retry-Fallback bei ungueltiger Session-ID
- Custom agents-Option entfernt (SDK spawnt Sub-Agents ohne Tools → Halluzination)
- Orchestrator-Prompt: verweist auf general-purpose (vollstaendiges Tool-Set)
Backend (claude.rs):
- claude_session_id NUR beim 1. Mal setzen (sonst verliert man History)
- Generic event emit fuer alle Bridge-Events ans Frontend
- Mode-Persistenz bei Bridge-Start (agent_mode aus DB laden)
Knowledge (knowledge.rs):
- MYSQL_HOST: 192.168.155.1 → 192.168.155.11 (MariaDB-Server)
- MYSQL_PASS: claude → 8715
- category Option<&str> Typ-Annotation fuer exec_map
Programs (programs.rs):
- xvfb_screenshot: Fallback scrot → import (ImageMagick) → ffmpeg
Voice (voice.rs):
- Part::file (existiert nicht) → Part::bytes, keine Temp-Datei
Frontend:
- events.ts: mode-changed Listener, result.text Fallback,
addAgent({id}) fuer korrekte Parent-Child-Verknuepfung
- ChatPanel: Copy-Button, Typing-Dots in Bubble (kein Doppel-Header),
$effect statt $:, onkeydown statt on:keydown
- AgentView: "Nur aktive" Toggle, Delegations-Badge, Tool-Count hidden bei 0,
agentMode Import
- ProgramsPanel: Button-Styling, Error-Banner mit Copy-Button,
selectable Text
- MonitorPanel: Filter-Dropdown Styling (Hintergrund + Hover)
- SettingsPanel: changeMode() wird beim Klick aufgerufen (nicht nur Store)
- +layout.svelte: agent_mode beim App-Start laden, Mode-Badge im Footer,
🎓-Button fuer Schulungsfenster
- +page.svelte: Programme-Tab + Hooks-Tab
Neue Dateien:
- TEST-ROADMAP.md — Status und naechste Schritte
- .gitignore erweitert (scheduled_tasks.lock, out/, node_modules)
- vscode-extension/tsconfig.json: include nur src/, exclude node_modules
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
350 lines
8.5 KiB
Svelte
350 lines
8.5 KiB
Svelte
<script lang="ts">
|
|
import '../app.css';
|
|
import { onMount, onDestroy } from 'svelte';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import { isProcessing, agentCount, currentModel, sessionStats, initEventListeners, cleanupEventListeners, currentSessionId, setMessagesFromDb, stickyContextInfo, agentMode, type DbMessage, type StickyContextInfo, type AgentMode } from '$lib/stores';
|
|
import StopButton from '$lib/components/StopButton.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;
|
|
}
|
|
|
|
onMount(async () => {
|
|
await initEventListeners();
|
|
|
|
// 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);
|
|
}
|
|
$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.toFixed(4)}`;
|
|
return `$${usd.toFixed(2)}`;
|
|
}
|
|
|
|
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 -->
|
|
<header class="titlebar">
|
|
<div class="titlebar-left">
|
|
<h1>Claude Desktop</h1>
|
|
</div>
|
|
<div class="titlebar-center">
|
|
{#if $isProcessing}
|
|
<span class="status-dot active"></span>
|
|
<span>Arbeitet...</span>
|
|
{:else}
|
|
<span class="status-dot idle"></span>
|
|
<span>Bereit</span>
|
|
{/if}
|
|
</div>
|
|
<div class="titlebar-right">
|
|
<button
|
|
class="teach-btn"
|
|
title="Schulungsmodus (Präsentations-Fenster)"
|
|
onclick={() => invoke('presentation_open').catch(e => console.error(e))}
|
|
>
|
|
🎓
|
|
</button>
|
|
{#if $currentModel}
|
|
<span class="model-badge">{$currentModel.replace('claude-', '').replace(/-\d{8}$/, '').replace(/(\D)-(\d)/g, '$1 $2').replace(/(\d)-(\d)/g, '$1.$2').replace(/\b[a-z]/g, c => c.toUpperCase())}</span>
|
|
{/if}
|
|
</div>
|
|
</header>
|
|
|
|
<main class="main-content">
|
|
<slot />
|
|
</main>
|
|
|
|
<!-- Footer: STOPP + Stats -->
|
|
<footer class="footer" class:active={$isProcessing}>
|
|
<StopButton on:click={handleStop} disabled={!$isProcessing} />
|
|
<div class="footer-stats">
|
|
{#if $stickyContextInfo?.loaded}
|
|
<span class="context-badge" title="Sticky Context: {$stickyContextInfo.entries} Einträge, ~{$stickyContextInfo.estimatedTokens} Token">
|
|
📌 +{$stickyContextInfo.estimatedTokens}ctx
|
|
</span>
|
|
<span class="sep">|</span>
|
|
{/if}
|
|
<span>Token: {formatTokens($sessionStats.totalTokensIn)} in / {formatTokens($sessionStats.totalTokensOut)} out</span>
|
|
<span class="sep">|</span>
|
|
<span>Kosten: {formatCost($sessionStats.totalCost)}</span>
|
|
<span class="sep">|</span>
|
|
<span>{$sessionStats.messageCount} Antworten</span>
|
|
{#if $currentModel}
|
|
<span class="sep">|</span>
|
|
<span class="model">{$currentModel.replace('claude-', '').replace(/-\d{8}$/, '').replace(/(\D)-(\d)/g, '$1 $2').replace(/(\d)-(\d)/g, '$1.$2').replace(/\b[a-z]/g, c => c.toUpperCase())}</span>
|
|
{/if}
|
|
{#if $agentMode && $agentMode !== 'solo'}
|
|
<span class="sep">|</span>
|
|
<span class="mode-badge mode-{$agentMode}" title="Agent-Modus: {$agentMode}">
|
|
{#if $agentMode === 'handlanger'}👷 Handlanger
|
|
{:else if $agentMode === 'experten'}🎓 Experten
|
|
{:else if $agentMode === 'auto'}🤖 Auto
|
|
{/if}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
|
|
<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: var(--spacing-xs) var(--spacing-md);
|
|
background: var(--bg-secondary);
|
|
border-bottom: 1px solid var(--border);
|
|
user-select: none;
|
|
height: 36px;
|
|
}
|
|
|
|
.titlebar h1 {
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
color: var(--text-heading);
|
|
}
|
|
|
|
.titlebar-center {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
font-size: 0.75rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.status-dot {
|
|
width: 7px;
|
|
height: 7px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.status-dot.active {
|
|
background: var(--success);
|
|
animation: pulse 1.5s ease-in-out infinite;
|
|
}
|
|
|
|
.status-dot.idle {
|
|
background: var(--text-secondary);
|
|
}
|
|
|
|
.model-badge {
|
|
padding: 1px 6px;
|
|
background: var(--bg-tertiary);
|
|
border-radius: var(--radius-sm);
|
|
font-size: 0.65rem;
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.titlebar-right {
|
|
display: flex;
|
|
gap: var(--spacing-xs);
|
|
font-size: 0.65rem;
|
|
color: var(--text-secondary);
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.sep {
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.main-content {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.footer {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
padding: var(--spacing-xs) var(--spacing-md);
|
|
background: var(--bg-secondary);
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.footer.active {
|
|
border-top: 2px solid var(--error);
|
|
}
|
|
|
|
.footer-stats {
|
|
display: flex;
|
|
gap: var(--spacing-sm);
|
|
font-size: 0.65rem;
|
|
color: var(--text-secondary);
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.footer-stats .sep {
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.footer-stats .model {
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.footer-stats .context-badge {
|
|
color: #22c55e;
|
|
font-weight: 500;
|
|
cursor: help;
|
|
}
|
|
|
|
.footer-stats .mode-badge {
|
|
font-weight: 600;
|
|
cursor: help;
|
|
padding: 1px 6px;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.footer-stats .mode-handlanger {
|
|
color: #f59e0b;
|
|
background: rgba(245, 158, 11, 0.12);
|
|
}
|
|
|
|
.footer-stats .mode-experten {
|
|
color: #a855f7;
|
|
background: rgba(168, 85, 247, 0.12);
|
|
}
|
|
|
|
.footer-stats .mode-auto {
|
|
color: #06b6d4;
|
|
background: rgba(6, 182, 212, 0.12);
|
|
}
|
|
|
|
.teach-btn {
|
|
background: transparent;
|
|
border: 1px solid var(--border);
|
|
color: var(--text-primary);
|
|
font-size: 0.9rem;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
margin-right: var(--spacing-xs);
|
|
}
|
|
|
|
.teach-btn:hover {
|
|
background: rgba(96, 165, 250, 0.15);
|
|
}
|
|
</style>
|