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,6 +99,8 @@
{/if}
{#if $isProcessing}
{@const lastMsg = $messages.at(-1)}
{#if !lastMsg || lastMsg.role !== 'assistant' || lastMsg.content === ''}
<div class="message assistant typing-msg">
<div class="message-header">
<span class="message-role">🤖 Claude</span>
@ -110,6 +112,7 @@
</div>
</div>
{/if}
{/if}
</div>
<div class="chat-input">

View file

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