UI: Aktivitätsanzeige mit Phasen statt nur "Denkt nach..." + kleine Dots
All checks were successful
Build AppImage / build (push) Successful in 7m34s

- Neue processingPhase Store: thinking, streaming, tool-use, subagent
- Phasen-Labels: "Denkt nach", "Schreibt Antwort", "Arbeitet", "Subagent aktiv"
- Tool-Details weiterhin inline (Liest Datei..., Führt aus..., etc.)
- Alte doppelte Bounce-Dots (8px) entfernt, ersetzt durch dezente 4px Pulse-Dots
- Alles in einer Zeile: Icon + Label + Dots

[appimage]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-21 09:00:47 +02:00
parent f287514af5
commit f94cfc287e
2 changed files with 106 additions and 68 deletions

View file

@ -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}
<div class="tool-status">
<span class="tool-icon">{getToolIcon($currentTool.tool)}</span>
<span class="tool-label">{getToolLabel($currentTool.tool, $currentTool.input)}</span>
</div>
{:else}
<div class="tool-status">
<span class="tool-icon">{'\u{1F9E0}'}</span>
<span class="tool-label">Denkt nach...</span>
</div>
{/if}
<span class="typing">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</span>
<div class="activity-indicator">
{#if $currentTool}
<span class="activity-icon">{getToolIcon($currentTool.tool)}</span>
<span class="activity-label">{getToolLabel($currentTool.tool, $currentTool.input)}</span>
{:else}
<span class="activity-icon">{getPhaseIcon($processingPhase)}</span>
<span class="activity-label">{getPhaseLabel($processingPhase)}</span>
{/if}
<span class="activity-dots">
<span class="activity-dot"></span>
<span class="activity-dot"></span>
<span class="activity-dot"></span>
</span>
</div>
{/if}
{:else}
{#if message.role === 'user' && message.content && shouldCollapse(message.content, 'user') && !expandedMessages.includes(message.id)}
@ -987,22 +1006,20 @@
<span class="message-role">{'\u{1F916}'} Claude</span>
</div>
<div class="message-content">
{#if $currentTool}
<div class="tool-status">
<span class="tool-icon">{getToolIcon($currentTool.tool)}</span>
<span class="tool-label">{getToolLabel($currentTool.tool, $currentTool.input)}</span>
</div>
{:else}
<div class="tool-status">
<span class="tool-icon">{'\u{1F9E0}'}</span>
<span class="tool-label">Denkt nach...</span>
</div>
{/if}
<span class="typing">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</span>
<div class="activity-indicator">
{#if $currentTool}
<span class="activity-icon">{getToolIcon($currentTool.tool)}</span>
<span class="activity-label">{getToolLabel($currentTool.tool, $currentTool.input)}</span>
{:else}
<span class="activity-icon">{getPhaseIcon($processingPhase)}</span>
<span class="activity-label">{getPhaseLabel($processingPhase)}</span>
{/if}
<span class="activity-dots">
<span class="activity-dot"></span>
<span class="activity-dot"></span>
<span class="activity-dot"></span>
</span>
</div>
</div>
</div>
{/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 {

View file

@ -34,6 +34,10 @@ import {
// Aktuell laufendes Tool (für inline Aktivitätsanzeige)
export const currentTool = writable<{ tool: string; input: Record<string, unknown> } | null>(null);
// Detaillierte Verarbeitungsphase für Status-Anzeige
export type ProcessingPhase = 'thinking' | 'streaming' | 'tool-use' | 'subagent' | 'idle';
export const processingPhase = writable<ProcessingPhase>('idle');
// Event-Typen vom Backend
interface AgentEvent {
id: string;
@ -145,6 +149,7 @@ export async function initEventListeners(): Promise<void> {
// 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<void> {
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<void> {
await listen<SubagentEvent>('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<void> {
// 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<void> {
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<void> {
listeners.push(
await listen<TextEvent>('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<void> {
agents.update((ags) => ags.map((a) => ({ ...a, status: 'stopped' as const })));
toolCalls.set([]);
currentTool.set(null);
processingPhase.set('idle');
isProcessing.set(false);
})
);