diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index a39ba23..26eae2c 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -920,17 +920,25 @@ const ChatScreen: React.FC = () => { setSearchIndex(0); }, [searchQuery]); - // Bei Index-Wechsel zu der entsprechenden Bubble scrollen + // Bei Index-Wechsel zu der entsprechenden Bubble scrollen. + // FlatList ist `inverted` → viewPosition 0.5 (mitte) ist beim inverted-Render + // tatsaechlich die Mitte des sichtbaren Bereichs. Wir verzoegern minimal + // damit Layout sicher fertig ist. useEffect(() => { if (!searchMatchIds.length) return; const id = searchMatchIds[searchIndex]; if (!id) return; - // invertedMessages → index in der angezeigten Liste finden const idx = invertedMessages.findIndex(m => m.id === id); if (idx < 0 || !flatListRef.current) return; - try { - flatListRef.current.scrollToIndex({ index: idx, animated: true, viewPosition: 0.4 }); - } catch {} + const tryScroll = () => { + try { + flatListRef.current?.scrollToIndex({ index: idx, animated: true, viewPosition: 0.5 }); + } catch { + // wird von onScrollToIndexFailed nochmal versucht + } + }; + // requestAnimationFrame statt setTimeout 0 — wartet auf naechsten Layout-Frame + requestAnimationFrame(tryScroll); }, [searchIndex, searchMatchIds, invertedMessages]); const activeSearchId = searchMatchIds[searchIndex] || ''; @@ -1440,10 +1448,14 @@ const ChatScreen: React.FC = () => { inverted data={invertedMessages} onScrollToIndexFailed={(info) => { - // Bei zu schnellem Aufruf vor Layout: einmal nachfassen + // FlatList kennt das Item-Layout noch nicht. Zuerst grob in die + // Naehe scrollen (Average-Item-Hoehe-Schaetzung), dann nach 250ms + // praezise nochmal versuchen. + const offset = info.averageItemLength * info.index; + try { flatListRef.current?.scrollToOffset({ offset, animated: false }); } catch {} setTimeout(() => { - try { flatListRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0.4 }); } catch {} - }, 200); + try { flatListRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0.5 }); } catch {} + }, 250); }} keyExtractor={item => item.id} renderItem={renderMessage} diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index fb4c4d7..553a3b0 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -1086,6 +1086,12 @@ class ARIABridge: except Exception as e: logger.error("[core] XTTS-Request fehlgeschlagen: %s — kein Audio", e) + # ARIA ist fertig — App's "ARIA denkt..." Indicator zurueck auf idle. + # _last_chat_final_at bewusst NICHT setzen: die 3s-Cooldown war fuer + # trailing OpenClaw-Activity-Events; bei Voice-Chat wuerde sie die + # naechste thinking-Welle unterdruecken. + await self._emit_activity("idle", "") + # ── Mode Persistence (global, nicht pro Geraet) ────── _MODE_FILE = "/shared/config/mode.json" @@ -1250,12 +1256,9 @@ class ARIABridge: # / Diagnostic-Reload als History-Quelle gelesen. self._append_chat_backup({"role": "user", "text": text, "source": source}) - # agent_activity broadcasten (App + Diagnostic "ARIA denkt..." Indicator) - await self._send_to_rvs({ - "type": "agent_activity", - "payload": {"activity": "thinking"}, - "timestamp": int(asyncio.get_event_loop().time() * 1000), - }) + # agent_activity → thinking. _emit_activity statt direktem _send_to_rvs + # damit der State-Cache fuer die spaetere idle-Dedup richtig steht. + await self._emit_activity("thinking", "") def _do_call(): try: @@ -1272,11 +1275,7 @@ class ARIABridge: status, body = await asyncio.get_event_loop().run_in_executor(None, _do_call) if status != 200: logger.error("[brain] /chat fehlgeschlagen: status=%s body=%s", status, body[:200]) - await self._send_to_rvs({ - "type": "agent_activity", - "payload": {"activity": "idle"}, - "timestamp": int(asyncio.get_event_loop().time() * 1000), - }) + await self._emit_activity("idle", "") await self._send_to_rvs({ "type": "chat", "payload": { @@ -1291,21 +1290,13 @@ class ARIABridge: data = json.loads(body) except Exception: logger.error("[brain] /chat lieferte ungueltiges JSON: %s", body[:200]) - await self._send_to_rvs({ - "type": "agent_activity", - "payload": {"activity": "idle"}, - "timestamp": int(asyncio.get_event_loop().time() * 1000), - }) + await self._emit_activity("idle", "") return reply = (data.get("reply") or "").strip() if not reply: logger.warning("[brain] /chat: leerer Reply") - await self._send_to_rvs({ - "type": "agent_activity", - "payload": {"activity": "idle"}, - "timestamp": int(asyncio.get_event_loop().time() * 1000), - }) + await self._emit_activity("idle", "") return # Side-Channel-Events VOR der Chat-Bubble broadcasten (z.B. skill_created) @@ -1331,6 +1322,8 @@ class ARIABridge: await self._process_core_response(reply, {}) except Exception: logger.exception("[brain] _process_core_response Fehler") + await self._emit_activity("idle", "") + # Originaler Fallback-Send (toter Code, _emit_activity uebernimmt jetzt) await self._send_to_rvs({ "type": "agent_activity", "payload": {"activity": "idle"}, @@ -2050,13 +2043,11 @@ class ARIABridge: if text.strip(): logger.info("[rvs] STT Ergebnis: '%s'", text[:80]) - # Hints (Barge-In, GPS) als Praefix vorschalten — gemeinsamer Helper - # mit dem chat-Pfad damit das Verhalten konsistent ist. - core_text = self._build_core_text(text, interrupted, location) - # ERST an aria-core senden (wichtigster Schritt) - await self.send_to_core(core_text, source="app-voice" + (" [barge-in]" if interrupted else "")) - # STT-Text an RVS senden (fuer Anzeige in App + Diagnostic) - # sender="stt" damit Bridge es ignoriert (kein Loop) + + # Reihenfolge wichtig: STT-Text ZUERST broadcasten damit die App + # die Voice-Bubble sofort mit dem erkannten Text aktualisieren + # kann — send_to_core blockt danach synchron auf Brain (kann + # dauern), wuerde sonst die Anzeige verzoegern. try: stt_payload = { "text": text, @@ -2080,6 +2071,10 @@ class ARIABridge: logger.warning("[rvs] STT-Text NICHT broadcastet — _send_to_rvs lieferte False") except Exception as e: logger.warning("[rvs] STT-Text konnte nicht an RVS gesendet werden: %s", e) + + # Dann an Brain — der blockt synchron bis ARIA fertig ist. + core_text = self._build_core_text(text, interrupted, location) + await self.send_to_core(core_text, source="app-voice" + (" [barge-in]" if interrupted else "")) else: logger.info("[rvs] Keine Sprache erkannt — ignoriert")