feat: Nachrichten während Claude arbeitet senden — wie in VS Code Extension

Bridge hat jetzt eine interne Pending-Queue: Nachrichten die während einer
laufenden query() eingehen werden gepuffert und nach dem aktuellen Turn
automatisch FIFO abgearbeitet. Kein Frontend-Queue mehr nötig.

User kann jetzt wie in Claude Code/VS Code Extension weiter tippen während
Claude arbeitet. Nachrichten erscheinen sofort im Chat und werden nahtlos
nach dem aktuellen Turn verarbeitet.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-22 08:36:21 +02:00
parent 61541098d7
commit f394d69b70
2 changed files with 59 additions and 68 deletions

View file

@ -36,6 +36,12 @@ if (!IS_DAEMON) process.stdin.resume();
let activeAbort = null;
let currentAgentId = null;
let currentModel = process.env.CLAUDE_MODEL || 'opus';
let isQueryRunning = false;
// Pending-Queue: Nachrichten die während einer laufenden query() eingehen
// werden hier gepuffert und nach dem aktuellen Turn automatisch abgearbeitet.
// So kann der User wie in Claude Code/VS Code Extension weiter tippen.
const pendingMessages = [];
// Agent-Modus (solo | handlanger | experten | auto)
let agentMode = 'solo';
@ -339,6 +345,20 @@ function summarizeToolInput(tool, input) {
// ============ Claude Agent SDK ============
async function sendMessage(message, requestId, model = null, contextOverride = null, resumeSessionId = null) {
// Wenn bereits eine query() läuft: Nachricht in Pending-Queue puffern
// und sofort bestätigen. Wird nach aktuellem Turn automatisch abgearbeitet.
if (isQueryRunning) {
pendingMessages.push({ message, requestId, model, contextOverride, resumeSessionId });
sendResponse(requestId, { status: 'queued', position: pendingMessages.length });
sendMonitorEvent('agent', `Nachricht gepuffert (Position ${pendingMessages.length})`, {
messageLength: message.length,
queueSize: pendingMessages.length,
});
return;
}
isQueryRunning = true;
// Modell für diese Anfrage (Parameter > State > Default)
const useModel = model || currentModel;
@ -767,6 +787,19 @@ async function sendMessage(message, requestId, model = null, contextOverride = n
sendEvent('all-stopped');
currentAgentId = null;
activeAbort = null;
isQueryRunning = false;
// Pending-Queue: Nächste Nachricht automatisch abarbeiten (FIFO)
if (pendingMessages.length > 0) {
const next = pendingMessages.shift();
sendMonitorEvent('agent', `Pending-Nachricht wird verarbeitet (${pendingMessages.length} verbleibend)`, {
messageLength: next.message.length,
remaining: pendingMessages.length,
});
// Asynchron starten — nicht awaiten damit finally sauber abschließt
sendMessage(next.message, next.requestId, next.model, next.contextOverride, next.resumeSessionId)
.catch(err => sendMonitorEvent('error', `Pending-Dispatch fehlgeschlagen: ${err.message}`, {}));
}
}
}

View file

@ -717,21 +717,8 @@
addMessage('system', `📋 Clipboard (${content_type}): ${suggestion}\n\`\`\`\n${preview}\n\`\`\``);
});
// Queue-Auto-Dispatch: sobald isProcessing von true auf false wechselt
// wird die naechste Nachricht aus der Queue abgeschickt (FIFO).
let lastProcessing = false;
const unsubProcessing = isProcessing.subscribe((val) => {
if (lastProcessing && !val) {
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;
});
// Legacy-Queue Subscriber (Sicherheitsnetz — Hauptlogik ist jetzt in der Bridge)
const unsubProcessing = isProcessing.subscribe(() => {});
return () => {
window.removeEventListener('keydown', handleGlobalKeydown);
@ -742,39 +729,23 @@
});
function cancelQueued() {
// Alle gequeuten Nachrichten verwerfen + aus dem Chat entfernen
// Legacy: Queue-Cancel (Bridge hat jetzt eigene Pending-Queue)
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: 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;
}
// Nachricht IMMER sofort senden — auch während Claude arbeitet.
// Die Bridge puffert die Nachricht intern und verarbeitet sie
// automatisch nach dem aktuellen Turn (wie in Claude Code/VS Code Extension).
await dispatchMessage(text);
}
// Den eigentlichen Send-Flow ausgelagert, damit er auch fuer die Queue genutzt wird.
// Den eigentlichen Send-Flow — sendet immer sofort an die Bridge.
// Wenn eine query() laeuft, puffert die Bridge die Nachricht intern.
async function dispatchMessage(text: string) {
// Auto-Session erstellen falls keine aktiv
let sessionId = get(currentSessionId);
@ -794,37 +765,31 @@
}
}
// 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,
role: 'user',
content: text,
timestamp: new Date(),
};
messages.update((msgs) => [...msgs, msg]);
// Sofort speichern (nicht auf Subscriber warten)
await saveMessageToDb(msg);
}
// User-Nachricht sofort im Chat anzeigen + in DB speichern
const msgId = crypto.randomUUID();
const msg: Message = {
id: msgId,
role: 'user',
content: text,
timestamp: new Date(),
};
messages.update((msgs) => [...msgs, msg]);
await saveMessageToDb(msg);
$currentInput = '';
$isProcessing = true;
// isProcessing nur setzen wenn nicht schon aktiv
// (bei gepufferten Nachrichten laeuft Claude ja schon)
if (!$isProcessing) {
$isProcessing = true;
}
try {
await invoke('send_message', { message: text });
} catch (err) {
console.error('Fehler beim Senden:', err);
addMessage('system', `Fehler: ${err}`);
// Nur auf false setzen wenn keine weiteren Nachrichten pending
$isProcessing = false;
}
}
@ -1239,18 +1204,11 @@
<span class="transcript-text">{liveTranscript}</span>
</div>
{/if}
{#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">{$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 eingeben — wird nach Antwort automatisch gesendet' : 'Nachricht eingeben... (Ctrl+K = Quick-Actions, Ctrl+Enter = Senden)'}
placeholder={$isProcessing ? 'Weiter tippen — wird nach aktuellem Turn verarbeitet...' : 'Nachricht eingeben... (Ctrl+K = Quick-Actions, Ctrl+Enter = Senden)'}
disabled={isRecording}
rows="3"
></textarea>