fix(voice): App uebernimmt Server-projectId der STT-Bubble — App/Diagnostic-Sync

Symptom: Frage im Projekt gestellt, Antwort nur in der App sichtbar, Frage nur
in der Diagnostic — Frage und Antwort scheinbar in verschiedenen Kontexten.

Ursache: die App taggt die lokale Voice-Bubble optimistisch mit dem App-Focus,
waehrend der Server (Voice-Router) die autoritative Zuordnung macht. Weichen
die ab (Registry-Race aus 3e88eec, Sticky, „fuer X:"-Prefix, Fallback), landet
die lokale App-Bubble in einem anderen Kontext als die serverseitig getaggte
Frage/Antwort → App- und Diagnostic-Ansicht divergieren.

Fix: beim Eintreffen des sender=stt-Broadcasts uebernimmt die App die
projectId des Servers fuer die Bubble (alle drei Match-Zweige). Nur wenn das
Feld mitkommt — altes Bridge-Format ohne projectId → App-Focus behalten.
Primaerursache bleibt 3e88eec (Router liefert wieder den korrekten Kontext).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-06 10:28:35 +02:00
parent b27fba316b
commit 5410371b9c
+14 -2
View File
@@ -1060,6 +1060,17 @@ const ChatScreen: React.FC = () => {
if (sender === 'stt') { if (sender === 'stt') {
const sttText = (message.payload.text as string) || ''; const sttText = (message.payload.text as string) || '';
const sttAudioReqId = (message.payload.audioRequestId as string) || ''; const sttAudioReqId = (message.payload.audioRequestId as string) || '';
// Autoritative Projekt-Zuordnung vom Server (Voice-Router). Die App
// hatte die lokale Bubble optimistisch mit dem App-Focus getaggt;
// wenn der Router anders entschieden hat (Sticky, \u201Efuer X:"-Prefix,
// oder Fallback), uebernehmen wir hier den Server-Wert \u2014 sonst
// divergieren App- und Diagnostic-Ansicht (Frage im einen Kontext,
// Antwort im anderen). Nur uebernehmen wenn das Feld mitgeliefert
// wurde (leer/undefined = altes Bridge-Format \u2192 App-Focus behalten).
const hasServerPid = typeof (message.payload as any).projectId === 'string';
const sttProjectId = ((message.payload as any).projectId as string) || '';
const applyPid = (m: ChatMessage): ChatMessage =>
hasServerPid ? { ...m, projectId: sttProjectId } : m;
if (!sttText) { if (!sttText) {
return; return;
} }
@@ -1072,7 +1083,7 @@ const ChatScreen: React.FC = () => {
const idxById = prev.findIndex(m => m.audioRequestId === sttAudioReqId); const idxById = prev.findIndex(m => m.audioRequestId === sttAudioReqId);
if (idxById >= 0) { if (idxById >= 0) {
const next = prev.slice(); const next = prev.slice();
next[idxById] = { ...next[idxById], text: newText }; next[idxById] = applyPid({ ...next[idxById], text: newText });
return next; return next;
} }
} }
@@ -1083,7 +1094,7 @@ const ChatScreen: React.FC = () => {
); );
if (idx >= 0) { if (idx >= 0) {
const next = prev.slice(); const next = prev.slice();
next[idx] = { ...next[idx], text: newText }; next[idx] = applyPid({ ...next[idx], text: newText });
return next; return next;
} }
// Letzter Fallback: gar keine Placeholder \u2192 neue Bubble einfuegen // Letzter Fallback: gar keine Placeholder \u2192 neue Bubble einfuegen
@@ -1093,6 +1104,7 @@ const ChatScreen: React.FC = () => {
text: newText, text: newText,
timestamp: message.timestamp, timestamp: message.timestamp,
attachments: [{ type: 'audio', name: 'Sprachaufnahme' }], attachments: [{ type: 'audio', name: 'Sprachaufnahme' }],
projectId: hasServerPid ? sttProjectId : focusedProjectIdRef.current,
}]); }]);
}); });
return; return;