feat: Multi-Queue — mehrere Nachrichten senden waehrend Claude arbeitet
All checks were successful
Build AppImage / build (push) Has been skipped
All checks were successful
Build AppImage / build (push) Has been skipped
Statt Single-Slot-Puffer jetzt FIFO-Queue: - Nachrichten erscheinen sofort im Chat (mit Queued-Markierung) - Werden automatisch nacheinander abgearbeitet - Send-Button + Mikrofon nie disabled waehrend Processing - Queue visuell mit Anzahl + Cancel-Button - Stopp loescht gesamte Queue Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
22bf5333af
commit
5b857ebba4
3 changed files with 77 additions and 34 deletions
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, queuedMessage, type Message } from '$lib/stores/app';
|
||||
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, queuedMessage, messageQueue, type Message } from '$lib/stores/app';
|
||||
import { marked, type Tokens } from 'marked';
|
||||
import { tick, onDestroy, onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
|
|
@ -450,14 +450,16 @@
|
|||
window.addEventListener('keydown', handleGlobalKeydown);
|
||||
|
||||
// Queue-Auto-Dispatch: sobald isProcessing von true auf false wechselt
|
||||
// und ein Puffer vorhanden ist, wird er automatisch abgeschickt.
|
||||
// wird die naechste Nachricht aus der Queue abgeschickt (FIFO).
|
||||
let lastProcessing = false;
|
||||
const unsubProcessing = isProcessing.subscribe((val) => {
|
||||
if (lastProcessing && !val) {
|
||||
const queued = get(queuedMessage);
|
||||
if (queued) {
|
||||
$queuedMessage = null;
|
||||
dispatchMessage(queued).catch((e) => console.error('Queue-Dispatch fehlgeschlagen:', e));
|
||||
const queue = get(messageQueue);
|
||||
if (queue.length > 0) {
|
||||
const [next, ...rest] = queue;
|
||||
messageQueue.set(rest);
|
||||
$queuedMessage = rest.length > 0 ? rest[0] : null;
|
||||
dispatchMessage(next).catch((e) => console.error('Queue-Dispatch fehlgeschlagen:', e));
|
||||
}
|
||||
}
|
||||
lastProcessing = val;
|
||||
|
|
@ -470,18 +472,31 @@
|
|||
});
|
||||
|
||||
function cancelQueued() {
|
||||
// Alle gequeuten Nachrichten verwerfen + aus dem Chat entfernen
|
||||
messageQueue.set([]);
|
||||
$queuedMessage = null;
|
||||
messages.update((msgs) => msgs.filter((m) => !(m as any).queued));
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
const text = $currentInput.trim();
|
||||
if (!text) return;
|
||||
|
||||
// Waehrend Claude antwortet: Single-Slot-Puffer statt Doppel-Send.
|
||||
// Der Subscriber weiter unten sendet den Puffer automatisch ab, sobald
|
||||
// isProcessing von true auf false wechselt.
|
||||
// Waehrend Claude antwortet: Nachricht in FIFO-Queue.
|
||||
// Sofort als User-Message im Chat anzeigen (mit queued-Marker).
|
||||
// Der Subscriber dispatcht automatisch wenn Claude fertig ist.
|
||||
if ($isProcessing) {
|
||||
messageQueue.update((q) => [...q, text]);
|
||||
$queuedMessage = text;
|
||||
// Sofort im Chat anzeigen damit User sieht dass die Nachricht angekommen ist
|
||||
const queuedMsg: Message = {
|
||||
id: crypto.randomUUID(),
|
||||
role: 'user',
|
||||
content: text,
|
||||
timestamp: new Date(),
|
||||
queued: true,
|
||||
};
|
||||
messages.update((msgs) => [...msgs, queuedMsg]);
|
||||
$currentInput = '';
|
||||
return;
|
||||
}
|
||||
|
|
@ -509,7 +524,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Nachricht hinzufügen (wird durch den Store-Subscriber gespeichert)
|
||||
// Pruefen ob die Nachricht schon als queued im Chat steht
|
||||
const existingQueued = get(messages).find((m) => m.queued && m.content === text);
|
||||
if (existingQueued) {
|
||||
// Queued-Markierung entfernen — Nachricht ist jetzt aktiv
|
||||
messages.update((msgs) =>
|
||||
msgs.map((m) => m.id === existingQueued.id ? { ...m, queued: false } : m)
|
||||
);
|
||||
// In DB speichern (wurde vorher nicht gespeichert da queued)
|
||||
await saveMessageToDb({ ...existingQueued, queued: false });
|
||||
} else {
|
||||
// Neue Nachricht hinzufuegen (normaler Send, nicht aus Queue)
|
||||
const msgId = crypto.randomUUID();
|
||||
const msg: Message = {
|
||||
id: msgId,
|
||||
|
|
@ -518,9 +543,9 @@
|
|||
timestamp: new Date(),
|
||||
};
|
||||
messages.update((msgs) => [...msgs, msg]);
|
||||
|
||||
// Sofort speichern (nicht auf Subscriber warten)
|
||||
await saveMessageToDb(msg);
|
||||
}
|
||||
|
||||
$currentInput = '';
|
||||
$isProcessing = true;
|
||||
|
|
@ -758,10 +783,12 @@
|
|||
</div>
|
||||
{:else}
|
||||
{#each $messages as message, index}
|
||||
<div class="message" class:user={message.role === 'user'} class:assistant={message.role === 'assistant'} class:system={message.role === 'system'} class:editing={editingMessageId === message.id}>
|
||||
<div class="message" class:user={message.role === 'user'} class:assistant={message.role === 'assistant'} class:system={message.role === 'system'} class:editing={editingMessageId === message.id} class:queued={message.queued}>
|
||||
<div class="message-header">
|
||||
<span class="message-role">
|
||||
{#if message.role === 'user'}
|
||||
{#if message.role === 'user' && message.queued}
|
||||
⏳ Du (wartet)
|
||||
{:else if message.role === 'user'}
|
||||
👤 Du
|
||||
{:else if message.role === 'assistant'}
|
||||
🤖 {#if message.model}{message.model.replace('claude-', '').replace(/-\d{8}$/, '').replace(/(\D)-(\d)/g, '$1 $2').replace(/(\d)-(\d)/g, '$1.$2').replace(/\b[a-z]/g, c => c.toUpperCase())}{:else}Claude{/if}
|
||||
|
|
@ -850,18 +877,18 @@
|
|||
<span class="transcript-text">{liveTranscript}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $queuedMessage}
|
||||
{#if $messageQueue.length > 0}
|
||||
<div class="queued-pill" title="Wird gesendet sobald Claude die aktuelle Antwort fertig hat">
|
||||
<span class="queued-icon">📬</span>
|
||||
<span class="queued-text">Nachricht wartet: „{$queuedMessage.slice(0, 80)}{$queuedMessage.length > 80 ? '…' : ''}"</span>
|
||||
<button class="queued-cancel" onclick={cancelQueued} aria-label="Wartende Nachricht verwerfen">✕</button>
|
||||
<span class="queued-text">{$messageQueue.length} Nachricht{$messageQueue.length > 1 ? 'en' : ''} in der Queue</span>
|
||||
<button class="queued-cancel" onclick={cancelQueued} aria-label="Wartende Nachrichten verwerfen">✕</button>
|
||||
</div>
|
||||
{/if}
|
||||
<textarea
|
||||
bind:this={inputTextarea}
|
||||
bind:value={$currentInput}
|
||||
onkeydown={handleKeydown}
|
||||
placeholder={$isProcessing ? 'Nachricht wird nach aktueller Antwort gesendet...' : 'Nachricht eingeben... (Ctrl+K = Focus, Ctrl+Enter = Senden)'}
|
||||
placeholder={$isProcessing ? 'Nachricht eingeben — wird nach Antwort automatisch gesendet' : 'Nachricht eingeben... (Ctrl+K = Focus, Ctrl+Enter = Senden)'}
|
||||
disabled={isRecording}
|
||||
rows="3"
|
||||
></textarea>
|
||||
|
|
@ -870,7 +897,6 @@
|
|||
class="mic-button"
|
||||
class:recording={isRecording}
|
||||
onclick={toggleRecording}
|
||||
disabled={$isProcessing}
|
||||
title={isRecording ? 'Aufnahme stoppen' : 'Spracheingabe starten'}
|
||||
>
|
||||
{#if isRecording}
|
||||
|
|
@ -886,7 +912,7 @@
|
|||
disabled={!$currentInput.trim() || isRecording}
|
||||
>
|
||||
{#if $isProcessing}
|
||||
⏳
|
||||
📬
|
||||
{:else}
|
||||
➤
|
||||
{/if}
|
||||
|
|
@ -1110,6 +1136,17 @@
|
|||
margin-left: auto;
|
||||
}
|
||||
|
||||
.message.queued {
|
||||
opacity: 0.6;
|
||||
border-left: 3px solid var(--accent, #f59e0b);
|
||||
animation: pulse-queued 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-queued {
|
||||
0%, 100% { opacity: 0.6; }
|
||||
50% { opacity: 0.8; }
|
||||
}
|
||||
|
||||
.message.assistant {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export interface Message {
|
|||
timestamp: Date;
|
||||
agentId?: string;
|
||||
model?: string;
|
||||
queued?: boolean; // Nachricht wartet in der Queue auf Dispatch
|
||||
}
|
||||
|
||||
export interface Permission {
|
||||
|
|
@ -58,9 +59,13 @@ export const selectedAgentId = writable<string | null>(null);
|
|||
export const currentModel = writable('');
|
||||
export const currentSessionId = writable<string | null>(null);
|
||||
|
||||
// Single-Slot-Puffer: wenn eine Nachricht abgeschickt wird waehrend Claude noch
|
||||
// antwortet, wird sie hier gehalten und nach Ende der aktuellen Antwort
|
||||
// automatisch gesendet. Neuere Inhalte ueberschreiben einen wartenden Slot.
|
||||
// Message-Queue: Nachrichten die waehrend der Verarbeitung eingehen, werden hier
|
||||
// gesammelt und nach Ende der aktuellen Antwort FIFO abgearbeitet.
|
||||
// Jede Nachricht erscheint sofort im Chat als User-Message.
|
||||
export const messageQueue = writable<string[]>([]);
|
||||
|
||||
// Abwaertskompatibel: queuedMessage zeigt die naechste wartende Nachricht
|
||||
// (Legacy — wird in ChatPanel noch referenziert)
|
||||
export const queuedMessage = writable<string | null>(null);
|
||||
|
||||
// Agent-Modus für Multi-Agent-Architektur
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import '../app.css';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { isProcessing, agentCount, currentModel, sessionStats, contextPercent, contextUsage, initEventListeners, cleanupEventListeners, currentSessionId, setMessagesFromDb, stickyContextInfo, agentMode, queuedMessage, type DbMessage, type StickyContextInfo, type AgentMode } from '$lib/stores';
|
||||
import { isProcessing, agentCount, currentModel, sessionStats, contextPercent, contextUsage, initEventListeners, cleanupEventListeners, currentSessionId, setMessagesFromDb, stickyContextInfo, agentMode, queuedMessage, messageQueue, type DbMessage, type StickyContextInfo, type AgentMode } from '$lib/stores';
|
||||
import StopButton from '$lib/components/StopButton.svelte';
|
||||
import UpdateDialog from '$lib/components/UpdateDialog.svelte';
|
||||
|
||||
|
|
@ -122,9 +122,10 @@
|
|||
} catch (err) {
|
||||
console.error('Fehler beim Stoppen:', err);
|
||||
}
|
||||
// Wartende Nachricht verwerfen — der User hat bewusst abgebrochen,
|
||||
// Wartende Nachrichten verwerfen — der User hat bewusst abgebrochen,
|
||||
// die Queue soll nicht automatisch nachfeuern.
|
||||
$queuedMessage = null;
|
||||
$messageQueue = [];
|
||||
$isProcessing = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue