UI: Aktivitätsanzeige mit Phasen statt nur "Denkt nach..." + kleine Dots
All checks were successful
Build AppImage / build (push) Successful in 7m34s
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:
parent
f287514af5
commit
f94cfc287e
2 changed files with 106 additions and 68 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue