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:
parent
61541098d7
commit
f394d69b70
2 changed files with 59 additions and 68 deletions
|
|
@ -36,6 +36,12 @@ if (!IS_DAEMON) process.stdin.resume();
|
||||||
let activeAbort = null;
|
let activeAbort = null;
|
||||||
let currentAgentId = null;
|
let currentAgentId = null;
|
||||||
let currentModel = process.env.CLAUDE_MODEL || 'opus';
|
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)
|
// Agent-Modus (solo | handlanger | experten | auto)
|
||||||
let agentMode = 'solo';
|
let agentMode = 'solo';
|
||||||
|
|
@ -339,6 +345,20 @@ function summarizeToolInput(tool, input) {
|
||||||
// ============ Claude Agent SDK ============
|
// ============ Claude Agent SDK ============
|
||||||
|
|
||||||
async function sendMessage(message, requestId, model = null, contextOverride = null, resumeSessionId = null) {
|
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)
|
// Modell für diese Anfrage (Parameter > State > Default)
|
||||||
const useModel = model || currentModel;
|
const useModel = model || currentModel;
|
||||||
|
|
||||||
|
|
@ -767,6 +787,19 @@ async function sendMessage(message, requestId, model = null, contextOverride = n
|
||||||
sendEvent('all-stopped');
|
sendEvent('all-stopped');
|
||||||
currentAgentId = null;
|
currentAgentId = null;
|
||||||
activeAbort = 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}`, {}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -717,21 +717,8 @@
|
||||||
addMessage('system', `📋 Clipboard (${content_type}): ${suggestion}\n\`\`\`\n${preview}\n\`\`\``);
|
addMessage('system', `📋 Clipboard (${content_type}): ${suggestion}\n\`\`\`\n${preview}\n\`\`\``);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Queue-Auto-Dispatch: sobald isProcessing von true auf false wechselt
|
// Legacy-Queue Subscriber (Sicherheitsnetz — Hauptlogik ist jetzt in der Bridge)
|
||||||
// wird die naechste Nachricht aus der Queue abgeschickt (FIFO).
|
const unsubProcessing = isProcessing.subscribe(() => {});
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', handleGlobalKeydown);
|
window.removeEventListener('keydown', handleGlobalKeydown);
|
||||||
|
|
@ -742,39 +729,23 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
function cancelQueued() {
|
function cancelQueued() {
|
||||||
// Alle gequeuten Nachrichten verwerfen + aus dem Chat entfernen
|
// Legacy: Queue-Cancel (Bridge hat jetzt eigene Pending-Queue)
|
||||||
messageQueue.set([]);
|
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: Nachricht in FIFO-Queue.
|
// Nachricht IMMER sofort senden — auch während Claude arbeitet.
|
||||||
// Sofort als User-Message im Chat anzeigen (mit queued-Marker).
|
// Die Bridge puffert die Nachricht intern und verarbeitet sie
|
||||||
// Der Subscriber dispatcht automatisch wenn Claude fertig ist.
|
// automatisch nach dem aktuellen Turn (wie in Claude Code/VS Code Extension).
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
await dispatchMessage(text);
|
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) {
|
async function dispatchMessage(text: string) {
|
||||||
// Auto-Session erstellen falls keine aktiv
|
// Auto-Session erstellen falls keine aktiv
|
||||||
let sessionId = get(currentSessionId);
|
let sessionId = get(currentSessionId);
|
||||||
|
|
@ -794,17 +765,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pruefen ob die Nachricht schon als queued im Chat steht
|
// User-Nachricht sofort im Chat anzeigen + in DB speichern
|
||||||
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,
|
||||||
|
|
@ -813,18 +774,22 @@
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
};
|
};
|
||||||
messages.update((msgs) => [...msgs, msg]);
|
messages.update((msgs) => [...msgs, msg]);
|
||||||
// Sofort speichern (nicht auf Subscriber warten)
|
|
||||||
await saveMessageToDb(msg);
|
await saveMessageToDb(msg);
|
||||||
}
|
|
||||||
|
|
||||||
$currentInput = '';
|
$currentInput = '';
|
||||||
|
|
||||||
|
// isProcessing nur setzen wenn nicht schon aktiv
|
||||||
|
// (bei gepufferten Nachrichten laeuft Claude ja schon)
|
||||||
|
if (!$isProcessing) {
|
||||||
$isProcessing = true;
|
$isProcessing = true;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await invoke('send_message', { message: text });
|
await invoke('send_message', { message: text });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Fehler beim Senden:', err);
|
console.error('Fehler beim Senden:', err);
|
||||||
addMessage('system', `Fehler: ${err}`);
|
addMessage('system', `Fehler: ${err}`);
|
||||||
|
// Nur auf false setzen wenn keine weiteren Nachrichten pending
|
||||||
$isProcessing = false;
|
$isProcessing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1239,18 +1204,11 @@
|
||||||
<span class="transcript-text">{liveTranscript}</span>
|
<span class="transcript-text">{liveTranscript}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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
|
<textarea
|
||||||
bind:this={inputTextarea}
|
bind:this={inputTextarea}
|
||||||
bind:value={$currentInput}
|
bind:value={$currentInput}
|
||||||
onkeydown={handleKeydown}
|
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}
|
disabled={isRecording}
|
||||||
rows="3"
|
rows="3"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue