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 { invoke } from '@tauri-apps/api/core';
import { emit } from '@tauri-apps/api/event'; import { emit } from '@tauri-apps/api/event';
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, queuedMessage, messageQueue, type Message } from '$lib/stores/app'; 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 { marked, type Tokens } from 'marked';
import { tick, onDestroy, onMount } from 'svelte'; import { tick, onDestroy, onMount } from 'svelte';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
@ -110,7 +110,7 @@
case 'Read': return `Liest ${(input.file_path as string)?.split('/').pop() || 'Datei'}...`; 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 'Write': return `Schreibt ${(input.file_path as string)?.split('/').pop() || 'Datei'}...`;
case 'Edit': return `Bearbeitet ${(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 'Grep': return `Sucht: ${(input.pattern as string) || ''}...`;
case 'Glob': return `Sucht Dateien: ${(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)}...`; 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 // Svelte Action: Copy-Buttons zu Code-Blöcken hinzufügen
function addCopyButtons(node: HTMLElement) { function addCopyButtons(node: HTMLElement) {
function processCodeBlocks() { function processCodeBlocks() {
@ -939,22 +960,20 @@
{/if} {/if}
{/if} {/if}
{:else if $isProcessing} {:else if $isProcessing}
{#if $currentTool} <div class="activity-indicator">
<div class="tool-status"> {#if $currentTool}
<span class="tool-icon">{getToolIcon($currentTool.tool)}</span> <span class="activity-icon">{getToolIcon($currentTool.tool)}</span>
<span class="tool-label">{getToolLabel($currentTool.tool, $currentTool.input)}</span> <span class="activity-label">{getToolLabel($currentTool.tool, $currentTool.input)}</span>
</div> {:else}
{:else} <span class="activity-icon">{getPhaseIcon($processingPhase)}</span>
<div class="tool-status"> <span class="activity-label">{getPhaseLabel($processingPhase)}</span>
<span class="tool-icon">{'\u{1F9E0}'}</span> {/if}
<span class="tool-label">Denkt nach...</span> <span class="activity-dots">
</div> <span class="activity-dot"></span>
{/if} <span class="activity-dot"></span>
<span class="typing"> <span class="activity-dot"></span>
<span class="dot"></span> </span>
<span class="dot"></span> </div>
<span class="dot"></span>
</span>
{/if} {/if}
{:else} {:else}
{#if message.role === 'user' && message.content && shouldCollapse(message.content, 'user') && !expandedMessages.includes(message.id)} {#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> <span class="message-role">{'\u{1F916}'} Claude</span>
</div> </div>
<div class="message-content"> <div class="message-content">
{#if $currentTool} <div class="activity-indicator">
<div class="tool-status"> {#if $currentTool}
<span class="tool-icon">{getToolIcon($currentTool.tool)}</span> <span class="activity-icon">{getToolIcon($currentTool.tool)}</span>
<span class="tool-label">{getToolLabel($currentTool.tool, $currentTool.input)}</span> <span class="activity-label">{getToolLabel($currentTool.tool, $currentTool.input)}</span>
</div> {:else}
{:else} <span class="activity-icon">{getPhaseIcon($processingPhase)}</span>
<div class="tool-status"> <span class="activity-label">{getPhaseLabel($processingPhase)}</span>
<span class="tool-icon">{'\u{1F9E0}'}</span> {/if}
<span class="tool-label">Denkt nach...</span> <span class="activity-dots">
</div> <span class="activity-dot"></span>
{/if} <span class="activity-dot"></span>
<span class="typing"> <span class="activity-dot"></span>
<span class="dot"></span> </span>
<span class="dot"></span> </div>
<span class="dot"></span>
</span>
</div> </div>
</div> </div>
{/if} {/if}
@ -1537,30 +1554,60 @@
border-radius: 2px; border-radius: 2px;
} }
/* Tool-Aktivitaetsanzeige */ /* Aktivitätsanzeige — kompakte Zeile mit Icon, Label und Dots */
.tool-status { .activity-indicator {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.4rem; gap: 0.5rem;
padding: 0.3rem 0.8rem; padding: 0.3rem 0;
margin: 0.2rem 0; margin: 0.2rem 0;
font-size: 0.7rem; font-size: 0.78rem;
color: var(--text-secondary, #9ca3af); color: var(--text-secondary, #9ca3af);
animation: fadeInTool 0.2s ease; animation: fadeInActivity 0.25s ease;
} }
.tool-icon { .activity-icon {
font-size: 0.75rem; font-size: 0.85rem;
flex-shrink: 0;
} }
.tool-label { .activity-label {
opacity: 0.8;
font-family: var(--font-mono, monospace); 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 { .activity-dots {
from { opacity: 0; transform: translateY(-4px); } 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); } to { opacity: 1; transform: translateY(0); }
} }
@ -1712,27 +1759,7 @@
font-weight: 600; font-weight: 600;
} }
/* Typing-Animation */ /* Alte Typing-Animation entfernt — ersetzt durch .activity-indicator */
.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); }
}
/* Input-Bereich */ /* Input-Bereich */
.chat-input { .chat-input {

View file

@ -34,6 +34,10 @@ import {
// Aktuell laufendes Tool (für inline Aktivitätsanzeige) // Aktuell laufendes Tool (für inline Aktivitätsanzeige)
export const currentTool = writable<{ tool: string; input: Record<string, unknown> } | null>(null); 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 // Event-Typen vom Backend
interface AgentEvent { interface AgentEvent {
id: string; id: string;
@ -145,6 +149,7 @@ export async function initEventListeners(): Promise<void> {
// WICHTIG: id mitgeben, sonst stimmt parentAgentId der Sub-Agents nicht! // WICHTIG: id mitgeben, sonst stimmt parentAgentId der Sub-Agents nicht!
addAgent(mapAgentType(type || 'main'), task || 'Verarbeite...', { id, model }); addAgent(mapAgentType(type || 'main'), task || 'Verarbeite...', { id, model });
isProcessing.set(true); isProcessing.set(true);
processingPhase.set('thinking');
// Leere Streaming-Nachricht anlegen // Leere Streaming-Nachricht anlegen
streamingMessageId = crypto.randomUUID(); streamingMessageId = crypto.randomUUID();
@ -187,6 +192,7 @@ export async function initEventListeners(): Promise<void> {
agents.update((ags) => ags.map((a) => ({ ...a, status: 'stopped' as const }))); agents.update((ags) => ags.map((a) => ({ ...a, status: 'stopped' as const })));
isProcessing.set(false); isProcessing.set(false);
currentTool.set(null); currentTool.set(null);
processingPhase.set('idle');
streamingMessageId = null; streamingMessageId = null;
}) })
); );
@ -196,6 +202,7 @@ export async function initEventListeners(): Promise<void> {
await listen<SubagentEvent>('subagent-started', (event) => { await listen<SubagentEvent>('subagent-started', (event) => {
const { id, parentAgentId, type, task, depth, model } = event.payload; const { id, parentAgentId, type, task, depth, model } = event.payload;
console.log('🤖 Subagent gestartet:', id, type, '(Parent:', parentAgentId, ')'); console.log('🤖 Subagent gestartet:', id, type, '(Parent:', parentAgentId, ')');
processingPhase.set('subagent');
addSubAgent( addSubAgent(
parentAgentId, parentAgentId,
@ -223,6 +230,7 @@ export async function initEventListeners(): Promise<void> {
// Inline-Aktivitätsanzeige aktualisieren // Inline-Aktivitätsanzeige aktualisieren
currentTool.set({ tool: tool || 'unknown', input: input || {} }); currentTool.set({ tool: tool || 'unknown', input: input || {} });
processingPhase.set('tool-use');
agents.update((ags) => { agents.update((ags) => {
const activeAgent = ags.find((a) => a.status === 'active'); 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; const { id, tool, success, output } = event.payload;
console.log('✅ Tool Ende:', id, success ? 'OK' : 'FEHLER'); console.log('✅ Tool Ende:', id, success ? 'OK' : 'FEHLER');
currentTool.set(null); currentTool.set(null);
processingPhase.set('thinking');
completeToolCall(id, output, !success); completeToolCall(id, output, !success);
// Hook: post-tool-use (fire-and-forget, Fehler blockieren nicht) // Hook: post-tool-use (fire-and-forget, Fehler blockieren nicht)
@ -312,6 +321,7 @@ export async function initEventListeners(): Promise<void> {
listeners.push( listeners.push(
await listen<TextEvent>('claude-text', (event) => { await listen<TextEvent>('claude-text', (event) => {
const { text } = event.payload; const { text } = event.payload;
processingPhase.set('streaming');
if (streamingMessageId) { if (streamingMessageId) {
messages.update((msgs) => messages.update((msgs) =>
msgs.map((m) => msgs.map((m) =>
@ -415,6 +425,7 @@ export async function initEventListeners(): Promise<void> {
agents.update((ags) => ags.map((a) => ({ ...a, status: 'stopped' as const }))); agents.update((ags) => ags.map((a) => ({ ...a, status: 'stopped' as const })));
toolCalls.set([]); toolCalls.set([]);
currentTool.set(null); currentTool.set(null);
processingPhase.set('idle');
isProcessing.set(false); isProcessing.set(false);
}) })
); );