From dc775ee34f6091267a379674bab190c283bc151a Mon Sep 17 00:00:00 2001 From: duffyduck Date: Fri, 17 Jul 2026 22:47:38 +0200 Subject: [PATCH] feat: geraetelokaler Voice-Projektwechsel ("geh in Projekt X") MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Voice-Projektwechsel ist zurueck, aber nur fuer das ausloesende Geraet: - App merkt sich die IDs eigener Anfragen (Text-clientMsgId via dispatchWithAck + Voice-audioRequestId an allen 4 Aufnahme-Stellen) in myRequestIdsRef. - Bridge haengt an project_changed die ausloesende clientMsgId an — in beiden Voice-Pfaden: send_to_core (ARIAs project_enter/exit) und _process_endpoint_text (Voice-Router back_to_main / project_prefix). - App folgt einem project_changed-Wechsel nur, wenn die clientMsgId eine eigene ist → andere App-Instanzen + Diagnostic bleiben unberuehrt. Nur Bridge + App betroffen. py-compile + tsc clean. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 13 +++++++++ android/package.json | 2 +- android/src/screens/ChatScreen.tsx | 42 ++++++++++++++++++++++++------ bridge/aria_bridge.py | 12 +++++++-- 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d65242..eca3672 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,19 @@ Alle Änderungen am Projekt. Format: [Keep a Changelog](https://keepachangelog.c --- +## [0.2.2.2] — 2026-07-17 — Voice-Projektwechsel zurück — aber gerätelokal + +### Hinzugefügt + +- **„ARIA, geh in Projekt X" per Sprache wechselt wieder das Projekt** — aber **nur auf dem Gerät, das den Befehl gab**. Andere App-Instanzen und das Diagnostic bleiben in ihrem Projekt. +- Umsetzung: Jedes Gerät merkt sich die IDs seiner eigenen Anfragen (Text-`clientMsgId` + Voice-`audioRequestId`). Der Brain/Voice-Router hängt an jedes `project_changed`-Event die auslösende `clientMsgId` an; die App folgt dem Wechsel nur, wenn die ID eine **eigene** ist. Deckt beide Voice-Pfade ab (ARIAs `project_enter`/`exit` via `send_to_core` **und** den Bridge-Voice-Router „für Projekt X: …" / „zurück zum Hauptchat"). +- Manuelles Umschalten bleibt wie gehabt gerätelokal über den Drawer. + +### Deploy +`git pull && docker compose up -d --build bridge` + APK 0.2.2.2 (kein Brain-Rebuild nötig). + +--- + ## [0.2.2.1] — 2026-07-17 — Projekt-Fokus wirklich pro Gerät unabhängig ### Behoben diff --git a/android/package.json b/android/package.json index c6f3ee4..09d9d12 100644 --- a/android/package.json +++ b/android/package.json @@ -1,6 +1,6 @@ { "name": "aria-cockpit", - "version": "0.2.2.1", + "version": "0.2.2.2", "private": true, "scripts": { "android": "react-native run-android", diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index 2730a9e..e3135b3 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -407,6 +407,22 @@ const ChatScreen: React.FC = () => { const nextClientMsgId = (): string => `cmsg_${Date.now()}_${Math.floor(Math.random() * 1_000_000)}`; + // IDs (clientMsgId + audioRequestId), die DIESES Geraet ausgeloest hat. + // Fuer den geraete-lokalen Voice-Projektwechsel: ein project_changed vom Brain + // traegt die ausloesende ID; nur wenn sie zu einer unserer IDs gehoert, folgt + // dieses Geraet dem Wechsel. So wechselt „ARIA, geh in Projekt X" nur das + // Geraet, das den Befehl gab — nicht andere App-/Diagnostic-Instanzen. + const myRequestIdsRef = useRef>(new Set()); + const rememberMyRequest = (id: string): void => { + if (!id) return; + const s = myRequestIdsRef.current; + s.add(id); + if (s.size > 80) { + const oldest = s.values().next().value; + if (oldest !== undefined) s.delete(oldest); + } + }; + // Wie lange wir auf das ACK warten bevor wir retryen. Bridge sollte // unmittelbar zurueckmelden — 30s ist grosszuegig fuer schlechte Netze. // 60s — grosszuegiger als 30s, weil langsame Brain-Calls (Multi-Tool) sonst @@ -462,6 +478,7 @@ const ChatScreen: React.FC = () => { return; } pendingPayloads.current.set(cmid, { type, payload }); + rememberMyRequest(cmid); const online = connectionStateRef.current === 'connected'; if (!online) { updateMessageStatus(cmid, { deliveryStatus: 'queued', sendAttempts: attempt }); @@ -992,14 +1009,19 @@ const ChatScreen: React.FC = () => { if (p.id && (p.kind === 'code' || p.kind === 'chat')) { projectFocus.setKind(p.id, p.kind); } - // KEIN erzwungener Fokuswechsel mehr: Projekt-Fokus ist rein GERAETE- - // LOKAL. project_changed-Broadcasts (von ARIA-Tools / Diagnostic / einer - // anderen App-Instanz) aktualisieren nur Namen + Typ, ziehen dieses - // Geraet aber NICHT in ein anderes Projekt. So arbeitet jede App-/ - // Diagnostic-Instanz unabhaengig in ihrem eigenen Projekt. Wechseln - // geht ausschliesslich lokal ueber den Projekt-Drawer. - // (action wird bewusst nicht mehr fuer setFocusedProjectId genutzt.) - void action; + // Projekt-Fokus ist GERAETE-LOKAL. Ein Broadcast wechselt dieses Geraet + // NUR, wenn er durch EINEN EIGENEN Befehl ausgeloest wurde — erkennbar an + // der mitgelieferten clientMsgId (unsere gesendete Text-cmid oder Voice- + // audioRequestId). So folgt „ARIA, geh in Projekt X" nur dem Geraet, das + // den Befehl gab; Diagnostic / andere App-Instanzen bleiben unberuehrt. + const trigCmid = typeof p.clientMsgId === 'string' ? p.clientMsgId : ''; + if (trigCmid && myRequestIdsRef.current.has(trigCmid)) { + if (action === 'entered' || action === 'created') { + if (p.id) setFocusedProjectId(p.id); + } else if (action === 'exited') { + setFocusedProjectId(''); + } + } return; } @@ -1623,6 +1645,7 @@ const ChatScreen: React.FC = () => { // dem finalen Text, dann wird die Bubble ueber audioRequestId-Match // aktualisiert (siehe chat-Handler oben). const audioRequestId = `audio_${Date.now()}_${Math.floor(Math.random() * 100000)}`; + rememberMyRequest(audioRequestId); const wasInterrupted = interruptAriaIfBusy(); const location = await getCurrentLocation(); const windowMs = await loadConvWindowMs(); @@ -1726,6 +1749,7 @@ const ChatScreen: React.FC = () => { // Kurze Pause damit halt durchgreift, dann neue Aufnahme starten await new Promise(r => setTimeout(r, 150)); const audioRequestId = `audio_${Date.now()}_${Math.floor(Math.random() * 100000)}`; + rememberMyRequest(audioRequestId); const location = await getCurrentLocation(); const windowMs = await loadConvWindowMs(); @@ -1802,6 +1826,7 @@ const ChatScreen: React.FC = () => { // useCallback damit der useEffect oben die Funktion stabil capturen kann. const startPassiveStreamingRecording = useCallback(async () => { const audioRequestId = `audio_passive_${Date.now()}_${Math.floor(Math.random() * 100000)}`; + rememberMyRequest(audioRequestId); const location = await getCurrentLocation(); const passiveMs = await loadPassiveListenMs(); const { ok } = await audioService.startStreamingRecording({ @@ -2205,6 +2230,7 @@ const ChatScreen: React.FC = () => { // ueber audioRequestId-Match nachgereicht wenn whisper das Endpoint feuert. const handleVoiceButtonStart = useCallback(async (): Promise => { const audioRequestId = `audio_${Date.now()}_${Math.floor(Math.random() * 100000)}`; + rememberMyRequest(audioRequestId); const wasInterrupted = interruptAriaIfBusy(); const location = await getCurrentLocation(); diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index de24f61..dadb01e 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -2007,10 +2007,15 @@ class ARIABridge: elif etype == "project_changed": # ARIA hat ein Projekt erstellt / betreten / verlassen / beendet. # App + Diagnostic refreshen ihren Projekt-Banner anhand des Events. + # clientMsgId der ausloesenden Anfrage mitgeben (Text-cmid oder + # Voice-audioRequestId) → das ausloesende Geraet kann einen + # geraete-lokalen Projektwechsel machen ("geh in Projekt X"), + # ohne andere App-/Diagnostic-Instanzen mitzuziehen. await self._send_to_rvs({ "type": "project_changed", "payload": { "action": event.get("action") or "", + "clientMsgId": client_msg_id or "", **(event.get("project") or {}), }, "timestamp": int(asyncio.get_event_loop().time() * 1000), @@ -3618,8 +3623,11 @@ class ARIABridge: ) if meta_action in ("back_to_main", "project_prefix"): - # UI-Focus-Update broadcasten - payload = {"action": "entered" if meta_action == "project_prefix" else "exited"} + # UI-Focus-Update broadcasten — clientMsgId (= audio_request_id der + # Aufnahme) mitgeben, damit NUR das Geraet folgt, das den Sprach- + # befehl gab (geraete-lokaler Fokus, keine anderen Instanzen ziehen). + payload = {"action": "entered" if meta_action == "project_prefix" else "exited", + "clientMsgId": audio_request_id or ""} if meta_action == "project_prefix" and project_id: # Namen aus dem Cache holen — best effort try: