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">
|
<script lang="ts">
|
||||||
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, 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 { 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';
|
||||||
|
|
@ -450,14 +450,16 @@
|
||||||
window.addEventListener('keydown', handleGlobalKeydown);
|
window.addEventListener('keydown', handleGlobalKeydown);
|
||||||
|
|
||||||
// Queue-Auto-Dispatch: sobald isProcessing von true auf false wechselt
|
// 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;
|
let lastProcessing = false;
|
||||||
const unsubProcessing = isProcessing.subscribe((val) => {
|
const unsubProcessing = isProcessing.subscribe((val) => {
|
||||||
if (lastProcessing && !val) {
|
if (lastProcessing && !val) {
|
||||||
const queued = get(queuedMessage);
|
const queue = get(messageQueue);
|
||||||
if (queued) {
|
if (queue.length > 0) {
|
||||||
$queuedMessage = null;
|
const [next, ...rest] = queue;
|
||||||
dispatchMessage(queued).catch((e) => console.error('Queue-Dispatch fehlgeschlagen:', e));
|
messageQueue.set(rest);
|
||||||
|
$queuedMessage = rest.length > 0 ? rest[0] : null;
|
||||||
|
dispatchMessage(next).catch((e) => console.error('Queue-Dispatch fehlgeschlagen:', e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastProcessing = val;
|
lastProcessing = val;
|
||||||
|
|
@ -470,18 +472,31 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
function cancelQueued() {
|
function cancelQueued() {
|
||||||
|
// Alle gequeuten Nachrichten verwerfen + aus dem Chat entfernen
|
||||||
|
messageQueue.set([]);
|
||||||
$queuedMessage = null;
|
$queuedMessage = null;
|
||||||
|
messages.update((msgs) => msgs.filter((m) => !(m as any).queued));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
const text = $currentInput.trim();
|
const text = $currentInput.trim();
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
// Waehrend Claude antwortet: Single-Slot-Puffer statt Doppel-Send.
|
// Waehrend Claude antwortet: Nachricht in FIFO-Queue.
|
||||||
// Der Subscriber weiter unten sendet den Puffer automatisch ab, sobald
|
// Sofort als User-Message im Chat anzeigen (mit queued-Marker).
|
||||||
// isProcessing von true auf false wechselt.
|
// Der Subscriber dispatcht automatisch wenn Claude fertig ist.
|
||||||
if ($isProcessing) {
|
if ($isProcessing) {
|
||||||
|
messageQueue.update((q) => [...q, text]);
|
||||||
$queuedMessage = 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 = '';
|
$currentInput = '';
|
||||||
return;
|
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 msgId = crypto.randomUUID();
|
||||||
const msg: Message = {
|
const msg: Message = {
|
||||||
id: msgId,
|
id: msgId,
|
||||||
|
|
@ -518,9 +543,9 @@
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
};
|
};
|
||||||
messages.update((msgs) => [...msgs, msg]);
|
messages.update((msgs) => [...msgs, msg]);
|
||||||
|
|
||||||
// Sofort speichern (nicht auf Subscriber warten)
|
// Sofort speichern (nicht auf Subscriber warten)
|
||||||
await saveMessageToDb(msg);
|
await saveMessageToDb(msg);
|
||||||
|
}
|
||||||
|
|
||||||
$currentInput = '';
|
$currentInput = '';
|
||||||
$isProcessing = true;
|
$isProcessing = true;
|
||||||
|
|
@ -758,10 +783,12 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each $messages as message, index}
|
{#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">
|
<div class="message-header">
|
||||||
<span class="message-role">
|
<span class="message-role">
|
||||||
{#if message.role === 'user'}
|
{#if message.role === 'user' && message.queued}
|
||||||
|
⏳ Du (wartet)
|
||||||
|
{:else if message.role === 'user'}
|
||||||
👤 Du
|
👤 Du
|
||||||
{:else if message.role === 'assistant'}
|
{: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}
|
🤖 {#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>
|
<span class="transcript-text">{liveTranscript}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $queuedMessage}
|
{#if $messageQueue.length > 0}
|
||||||
<div class="queued-pill" title="Wird gesendet sobald Claude die aktuelle Antwort fertig hat">
|
<div class="queued-pill" title="Wird gesendet sobald Claude die aktuelle Antwort fertig hat">
|
||||||
<span class="queued-icon">📬</span>
|
<span class="queued-icon">📬</span>
|
||||||
<span class="queued-text">Nachricht wartet: „{$queuedMessage.slice(0, 80)}{$queuedMessage.length > 80 ? '…' : ''}"</span>
|
<span class="queued-text">{$messageQueue.length} Nachricht{$messageQueue.length > 1 ? 'en' : ''} in der Queue</span>
|
||||||
<button class="queued-cancel" onclick={cancelQueued} aria-label="Wartende Nachricht verwerfen">✕</button>
|
<button class="queued-cancel" onclick={cancelQueued} aria-label="Wartende Nachrichten verwerfen">✕</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<textarea
|
<textarea
|
||||||
bind:this={inputTextarea}
|
bind:this={inputTextarea}
|
||||||
bind:value={$currentInput}
|
bind:value={$currentInput}
|
||||||
onkeydown={handleKeydown}
|
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}
|
disabled={isRecording}
|
||||||
rows="3"
|
rows="3"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
@ -870,7 +897,6 @@
|
||||||
class="mic-button"
|
class="mic-button"
|
||||||
class:recording={isRecording}
|
class:recording={isRecording}
|
||||||
onclick={toggleRecording}
|
onclick={toggleRecording}
|
||||||
disabled={$isProcessing}
|
|
||||||
title={isRecording ? 'Aufnahme stoppen' : 'Spracheingabe starten'}
|
title={isRecording ? 'Aufnahme stoppen' : 'Spracheingabe starten'}
|
||||||
>
|
>
|
||||||
{#if isRecording}
|
{#if isRecording}
|
||||||
|
|
@ -886,7 +912,7 @@
|
||||||
disabled={!$currentInput.trim() || isRecording}
|
disabled={!$currentInput.trim() || isRecording}
|
||||||
>
|
>
|
||||||
{#if $isProcessing}
|
{#if $isProcessing}
|
||||||
⏳
|
📬
|
||||||
{:else}
|
{:else}
|
||||||
➤
|
➤
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -1110,6 +1136,17 @@
|
||||||
margin-left: auto;
|
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 {
|
.message.assistant {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ export interface Message {
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
queued?: boolean; // Nachricht wartet in der Queue auf Dispatch
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Permission {
|
export interface Permission {
|
||||||
|
|
@ -58,9 +59,13 @@ export const selectedAgentId = writable<string | null>(null);
|
||||||
export const currentModel = writable('');
|
export const currentModel = writable('');
|
||||||
export const currentSessionId = writable<string | null>(null);
|
export const currentSessionId = writable<string | null>(null);
|
||||||
|
|
||||||
// Single-Slot-Puffer: wenn eine Nachricht abgeschickt wird waehrend Claude noch
|
// Message-Queue: Nachrichten die waehrend der Verarbeitung eingehen, werden hier
|
||||||
// antwortet, wird sie hier gehalten und nach Ende der aktuellen Antwort
|
// gesammelt und nach Ende der aktuellen Antwort FIFO abgearbeitet.
|
||||||
// automatisch gesendet. Neuere Inhalte ueberschreiben einen wartenden Slot.
|
// 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);
|
export const queuedMessage = writable<string | null>(null);
|
||||||
|
|
||||||
// Agent-Modus für Multi-Agent-Architektur
|
// Agent-Modus für Multi-Agent-Architektur
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
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 StopButton from '$lib/components/StopButton.svelte';
|
||||||
import UpdateDialog from '$lib/components/UpdateDialog.svelte';
|
import UpdateDialog from '$lib/components/UpdateDialog.svelte';
|
||||||
|
|
||||||
|
|
@ -122,9 +122,10 @@
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Fehler beim Stoppen:', 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.
|
// die Queue soll nicht automatisch nachfeuern.
|
||||||
$queuedMessage = null;
|
$queuedMessage = null;
|
||||||
|
$messageQueue = [];
|
||||||
$isProcessing = false;
|
$isProcessing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue