diff --git a/src/lib/components/ChatPanel.svelte b/src/lib/components/ChatPanel.svelte index 961ebc4..387703e 100644 --- a/src/lib/components/ChatPanel.svelte +++ b/src/lib/components/ChatPanel.svelte @@ -2,7 +2,7 @@ import { invoke } from '@tauri-apps/api/core'; import { emit } from '@tauri-apps/api/event'; import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, queuedMessage, messageQueue, type Message } from '$lib/stores/app'; - import { currentTool } from '$lib/stores/events'; + import { currentTool, processingPhase } from '$lib/stores/events'; import { marked, type Tokens } from 'marked'; import { tick, onDestroy, onMount } from 'svelte'; import { get } from 'svelte/store'; @@ -110,7 +110,7 @@ case 'Read': return `Liest ${(input.file_path as string)?.split('/').pop() || 'Datei'}...`; case 'Write': return `Schreibt ${(input.file_path as string)?.split('/').pop() || 'Datei'}...`; case 'Edit': return `Bearbeitet ${(input.file_path as string)?.split('/').pop() || 'Datei'}...`; - case 'Bash': return `Fuehrt aus: ${((input.command as string) || '').substring(0, 40)}...`; + case 'Bash': return `Führt aus: ${((input.command as string) || '').substring(0, 40)}...`; case 'Grep': return `Sucht: ${(input.pattern as string) || ''}...`; case 'Glob': return `Sucht Dateien: ${(input.pattern as string) || ''}...`; case 'Task': case 'Agent': return `Delegiert: ${((input.description as string) || (input.prompt as string) || '').substring(0, 40)}...`; @@ -118,6 +118,27 @@ } } + // Phasen-basierte Status-Anzeige + function getPhaseIcon(phase: string): string { + switch(phase) { + case 'thinking': return '\u{1F9E0}'; // 🧠 + case 'streaming': return '\u{270D}\u{FE0F}'; // ✍️ + case 'tool-use': return '\u{1F527}'; // 🔧 + case 'subagent': return '\u{1F916}'; // 🤖 + default: return '\u{2699}\u{FE0F}'; // ⚙️ + } + } + + function getPhaseLabel(phase: string): string { + switch(phase) { + case 'thinking': return 'Denkt nach'; + case 'streaming': return 'Schreibt Antwort'; + case 'tool-use': return 'Arbeitet'; + case 'subagent': return 'Subagent aktiv'; + default: return 'Verarbeitet'; + } + } + // Svelte Action: Copy-Buttons zu Code-Blöcken hinzufügen function addCopyButtons(node: HTMLElement) { function processCodeBlocks() { @@ -939,22 +960,20 @@ {/if} {/if} {:else if $isProcessing} - {#if $currentTool} -
- {getToolIcon($currentTool.tool)} - {getToolLabel($currentTool.tool, $currentTool.input)} -
- {:else} -
- {'\u{1F9E0}'} - Denkt nach... -
- {/if} - - - - - +
+ {#if $currentTool} + {getToolIcon($currentTool.tool)} + {getToolLabel($currentTool.tool, $currentTool.input)} + {:else} + {getPhaseIcon($processingPhase)} + {getPhaseLabel($processingPhase)} + {/if} + + + + + +
{/if} {:else} {#if message.role === 'user' && message.content && shouldCollapse(message.content, 'user') && !expandedMessages.includes(message.id)} @@ -987,22 +1006,20 @@ {'\u{1F916}'} Claude
- {#if $currentTool} -
- {getToolIcon($currentTool.tool)} - {getToolLabel($currentTool.tool, $currentTool.input)} -
- {:else} -
- {'\u{1F9E0}'} - Denkt nach... -
- {/if} - - - - - +
+ {#if $currentTool} + {getToolIcon($currentTool.tool)} + {getToolLabel($currentTool.tool, $currentTool.input)} + {:else} + {getPhaseIcon($processingPhase)} + {getPhaseLabel($processingPhase)} + {/if} + + + + + +
{/if} @@ -1537,30 +1554,60 @@ border-radius: 2px; } - /* Tool-Aktivitaetsanzeige */ - .tool-status { + /* Aktivitätsanzeige — kompakte Zeile mit Icon, Label und Dots */ + .activity-indicator { display: flex; align-items: center; - gap: 0.4rem; - padding: 0.3rem 0.8rem; + gap: 0.5rem; + padding: 0.3rem 0; margin: 0.2rem 0; - font-size: 0.7rem; + font-size: 0.78rem; color: var(--text-secondary, #9ca3af); - animation: fadeInTool 0.2s ease; + animation: fadeInActivity 0.25s ease; } - .tool-icon { - font-size: 0.75rem; + .activity-icon { + font-size: 0.85rem; + flex-shrink: 0; } - .tool-label { - opacity: 0.8; + .activity-label { font-family: var(--font-mono, monospace); - font-size: 0.65rem; + font-size: 0.72rem; + opacity: 0.85; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 280px; } - @keyframes fadeInTool { - from { opacity: 0; transform: translateY(-4px); } + .activity-dots { + display: inline-flex; + gap: 3px; + margin-left: 2px; + flex-shrink: 0; + } + + .activity-dot { + width: 4px; + height: 4px; + background: currentColor; + border-radius: 50%; + opacity: 0.5; + animation: dotPulse 1.2s infinite ease-in-out; + } + + .activity-dot:nth-child(1) { animation-delay: 0s; } + .activity-dot:nth-child(2) { animation-delay: 0.2s; } + .activity-dot:nth-child(3) { animation-delay: 0.4s; } + + @keyframes dotPulse { + 0%, 60%, 100% { opacity: 0.3; transform: scale(1); } + 30% { opacity: 1; transform: scale(1.3); } + } + + @keyframes fadeInActivity { + from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: translateY(0); } } @@ -1712,27 +1759,7 @@ font-weight: 600; } - /* Typing-Animation */ - .typing { - display: flex; - gap: 4px; - } - - .dot { - width: 8px; - height: 8px; - background: var(--text-secondary); - border-radius: 50%; - animation: bounce 1.4s infinite ease-in-out; - } - - .dot:nth-child(1) { animation-delay: -0.32s; } - .dot:nth-child(2) { animation-delay: -0.16s; } - - @keyframes bounce { - 0%, 80%, 100% { transform: scale(0); } - 40% { transform: scale(1); } - } + /* Alte Typing-Animation entfernt — ersetzt durch .activity-indicator */ /* Input-Bereich */ .chat-input { diff --git a/src/lib/stores/events.ts b/src/lib/stores/events.ts index 6e7784c..64b53d1 100644 --- a/src/lib/stores/events.ts +++ b/src/lib/stores/events.ts @@ -34,6 +34,10 @@ import { // Aktuell laufendes Tool (für inline Aktivitätsanzeige) export const currentTool = writable<{ tool: string; input: Record } | null>(null); +// Detaillierte Verarbeitungsphase für Status-Anzeige +export type ProcessingPhase = 'thinking' | 'streaming' | 'tool-use' | 'subagent' | 'idle'; +export const processingPhase = writable('idle'); + // Event-Typen vom Backend interface AgentEvent { id: string; @@ -145,6 +149,7 @@ export async function initEventListeners(): Promise { // WICHTIG: id mitgeben, sonst stimmt parentAgentId der Sub-Agents nicht! addAgent(mapAgentType(type || 'main'), task || 'Verarbeite...', { id, model }); isProcessing.set(true); + processingPhase.set('thinking'); // Leere Streaming-Nachricht anlegen streamingMessageId = crypto.randomUUID(); @@ -187,6 +192,7 @@ export async function initEventListeners(): Promise { agents.update((ags) => ags.map((a) => ({ ...a, status: 'stopped' as const }))); isProcessing.set(false); currentTool.set(null); + processingPhase.set('idle'); streamingMessageId = null; }) ); @@ -196,6 +202,7 @@ export async function initEventListeners(): Promise { await listen('subagent-started', (event) => { const { id, parentAgentId, type, task, depth, model } = event.payload; console.log('🤖 Subagent gestartet:', id, type, '(Parent:', parentAgentId, ')'); + processingPhase.set('subagent'); addSubAgent( parentAgentId, @@ -223,6 +230,7 @@ export async function initEventListeners(): Promise { // Inline-Aktivitätsanzeige aktualisieren currentTool.set({ tool: tool || 'unknown', input: input || {} }); + processingPhase.set('tool-use'); agents.update((ags) => { const activeAgent = ags.find((a) => a.status === 'active'); @@ -272,6 +280,7 @@ export async function initEventListeners(): Promise { const { id, tool, success, output } = event.payload; console.log('✅ Tool Ende:', id, success ? 'OK' : 'FEHLER'); currentTool.set(null); + processingPhase.set('thinking'); completeToolCall(id, output, !success); // Hook: post-tool-use (fire-and-forget, Fehler blockieren nicht) @@ -312,6 +321,7 @@ export async function initEventListeners(): Promise { listeners.push( await listen('claude-text', (event) => { const { text } = event.payload; + processingPhase.set('streaming'); if (streamingMessageId) { messages.update((msgs) => msgs.map((m) => @@ -415,6 +425,7 @@ export async function initEventListeners(): Promise { agents.update((ags) => ags.map((a) => ({ ...a, status: 'stopped' as const }))); toolCalls.set([]); currentTool.set(null); + processingPhase.set('idle'); isProcessing.set(false); }) );