Live-Streaming im Chat — Text erscheint Wort für Wort

- events.ts: Leere Streaming-Nachricht bei agent-started anlegen,
  claude-text Events schreiben direkt in die aktuelle Nachricht
- ChatPanel: Typing-Dots nur bei leerer/fehlender Streaming-Nachricht
- Kein Warten auf agent-stopped mehr — Text erscheint sofort

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-13 20:34:38 +02:00
parent 3e5021dbf0
commit df3b33a6ed
2 changed files with 48 additions and 38 deletions

View file

@ -99,16 +99,19 @@
{/if} {/if}
{#if $isProcessing} {#if $isProcessing}
<div class="message assistant typing-msg"> {@const lastMsg = $messages.at(-1)}
<div class="message-header"> {#if !lastMsg || lastMsg.role !== 'assistant' || lastMsg.content === ''}
<span class="message-role">🤖 Claude</span> <div class="message assistant typing-msg">
<div class="message-header">
<span class="message-role">🤖 Claude</span>
</div>
<div class="message-content typing">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
</div> </div>
<div class="message-content typing"> {/if}
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
</div>
{/if} {/if}
</div> </div>

View file

@ -41,20 +41,18 @@ interface ResultEvent {
input: number; input: number;
output: number; output: number;
}; };
session_id?: string;
} }
// Listener-Handles // Listener-Handles
let listeners: UnlistenFn[] = []; let listeners: UnlistenFn[] = [];
// Aktuelle Nachricht (wird während Streaming aufgebaut) // Streaming: ID der aktuellen Live-Nachricht
let currentResponseText = ''; let streamingMessageId: string | null = null;
let currentResponseAgentId: string | null = null;
// Events initialisieren // Events initialisieren
export async function initEventListeners(): Promise<void> { export async function initEventListeners(): Promise<void> {
console.log('🎧 Initialisiere Event-Listener...'); console.log('🎧 Initialisiere Event-Listener...');
// Aufräumen falls bereits initialisiert
await cleanupEventListeners(); await cleanupEventListeners();
// Bridge bereit // Bridge bereit
@ -70,10 +68,21 @@ export async function initEventListeners(): Promise<void> {
const { id, type, task } = event.payload; const { id, type, task } = event.payload;
console.log('🤖 Agent gestartet:', id, type); console.log('🤖 Agent gestartet:', id, type);
const agentType = mapAgentType(type || 'main'); addAgent(mapAgentType(type || 'main'), task || 'Verarbeite...');
addAgent(agentType, task || 'Verarbeite...');
currentResponseAgentId = id;
isProcessing.set(true); isProcessing.set(true);
// Leere Streaming-Nachricht anlegen
streamingMessageId = crypto.randomUUID();
messages.update((msgs) => [
...msgs,
{
id: streamingMessageId!,
role: 'assistant',
content: '',
timestamp: new Date(),
agentId: id
}
]);
}) })
); );
@ -82,15 +91,8 @@ export async function initEventListeners(): Promise<void> {
await listen<AgentEvent>('agent-stopped', (event) => { await listen<AgentEvent>('agent-stopped', (event) => {
const { id } = event.payload; const { id } = event.payload;
console.log('⏹️ Agent gestoppt:', id); console.log('⏹️ Agent gestoppt:', id);
updateAgentStatus(id, 'stopped'); updateAgentStatus(id, 'stopped');
streamingMessageId = null;
// Falls das der Haupt-Agent war, Antwort finalisieren
if (currentResponseAgentId === id && currentResponseText) {
addMessage('assistant', currentResponseText, id);
currentResponseText = '';
currentResponseAgentId = null;
}
// Prüfen ob noch Agents aktiv // Prüfen ob noch Agents aktiv
agents.update((ags) => { agents.update((ags) => {
@ -109,21 +111,16 @@ export async function initEventListeners(): Promise<void> {
console.log('⏹️ Alle Agents gestoppt'); console.log('⏹️ Alle Agents gestoppt');
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);
streamingMessageId = null;
if (currentResponseText) {
addMessage('assistant', currentResponseText);
currentResponseText = '';
}
}) })
); );
// Tool Start // Tool Start
listeners.push( listeners.push(
await listen<ToolEvent>('tool-start', (event) => { await listen<ToolEvent>('tool-start', (event) => {
const { id, tool, input } = event.payload; const { tool, input } = event.payload;
console.log('🔧 Tool Start:', tool); console.log('🔧 Tool Start:', tool);
// Dem aktiven Haupt-Agent zuordnen
agents.update((ags) => { agents.update((ags) => {
const activeAgent = ags.find((a) => a.status === 'active'); const activeAgent = ags.find((a) => a.status === 'active');
if (activeAgent) { if (activeAgent) {
@ -139,34 +136,43 @@ export async function initEventListeners(): Promise<void> {
await listen<ToolEvent>('tool-end', (event) => { await listen<ToolEvent>('tool-end', (event) => {
const { id, success, output } = event.payload; const { id, success, output } = event.payload;
console.log('✅ Tool Ende:', id, success ? 'OK' : 'FEHLER'); console.log('✅ Tool Ende:', id, success ? 'OK' : 'FEHLER');
completeToolCall(id, output, !success); completeToolCall(id, output, !success);
}) })
); );
// Text-Streaming // Text-Streaming — live in die aktuelle Nachricht schreiben
listeners.push( listeners.push(
await listen<TextEvent>('claude-text', (event) => { await listen<TextEvent>('claude-text', (event) => {
const { text } = event.payload; const { text } = event.payload;
currentResponseText += text; if (streamingMessageId) {
messages.update((msgs) =>
msgs.map((m) =>
m.id === streamingMessageId
? { ...m, content: m.content + text }
: m
)
);
}
}) })
); );
// Ergebnis (Kosten, Token) // Ergebnis (Kosten, Token)
listeners.push( listeners.push(
await listen<ResultEvent>('claude-result', (event) => { await listen<ResultEvent>('claude-result', (event) => {
const { cost, tokens } = event.payload; const { cost, tokens, session_id } = event.payload;
console.log('📊 Ergebnis:', { console.log('📊 Ergebnis:', {
cost: cost ? `$${cost.toFixed(4)}` : 'unbekannt', cost: cost ? `$${cost.toFixed(4)}` : 'unbekannt',
tokens tokens,
session_id
}); });
}) })
); );
// Agents gestoppt (vom STOPP-Button) // STOPP-Signal
listeners.push( listeners.push(
await listen('agents-stopped', () => { await listen('agents-stopped', () => {
console.log('🛑 STOPP-Signal empfangen'); console.log('🛑 STOPP-Signal empfangen');
streamingMessageId = null;
clearAll(); clearAll();
}) })
); );
@ -187,6 +193,7 @@ function mapAgentType(type: string): 'main' | 'explore' | 'plan' | 'bash' {
const typeMap: Record<string, 'main' | 'explore' | 'plan' | 'bash'> = { const typeMap: Record<string, 'main' | 'explore' | 'plan' | 'bash'> = {
main: 'main', main: 'main',
'Main Agent': 'main', 'Main Agent': 'main',
Main: 'main',
explore: 'explore', explore: 'explore',
Explore: 'explore', Explore: 'explore',
plan: 'plan', plan: 'plan',