Compare commits

...
2 Commits
Author SHA1 Message Date
duffyduckandClaude Opus 4.8 dc775ee34f feat: geraetelokaler Voice-Projektwechsel ("geh in Projekt X")
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 <noreply@anthropic.com>
2026-07-17 22:47:38 +02:00
duffyduckandClaude Opus 4.8 c2122c43c2 fix: Projekt-Fokus pro Geraet unabhaengig (Diagnostic/App/Multi-Instanz)
Zwei Kopplungs-Bugs behoben:
- set_project_kind nutzte den globalen active_project statt des Request-Projekts
  → markierte das falsche Projekt (belegt: global aktiv war mac_os_update_fehler,
  das kind=code bekam, obwohl die App in basic_os war). _dispatch_tool bekommt
  jetzt die project_id des Requests durchgereicht; set_project_kind wirkt darauf.
- Die App erzwang bei project_changed-Broadcasts (ARIA-Tool/Diagnostic/andere
  App-Instanz) einen Fokuswechsel → alle Geraete wurden mitgezogen. Fokus ist
  jetzt rein geraetelokal; Broadcasts aktualisieren nur Namen/Typ. Wechseln nur
  noch lokal ueber den Drawer.

py-compile + tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 22:37:51 +02:00
5 changed files with 85 additions and 15 deletions
+26
View File
@@ -10,6 +10,32 @@ 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
- **`set_project_kind` markierte das falsche Projekt.** Es nutzte den **globalen** `active_project` (geräteübergreifend) statt des Projekts, in dem der aktuelle Request läuft. Folge: Obwohl auf der App „Basic os" aktiv war, bekam das global-aktive „Mac OS Update Fehler" `kind=code`. Jetzt wirkt das Tool auf die pro Request mitgesendete `project_id` (in `_dispatch_tool` durchgereicht) — jede App-/Diagnostic-Instanz markiert ihr eigenes Projekt.
- **App wurde von fremden `project_changed`-Broadcasts ins andere Projekt gezogen.** Ein Projektwechsel/‑anlegen durch ARIA, das Diagnostic oder eine andere App-Instanz erzwang auf **allen** Geräten einen Fokuswechsel. Der Projekt-Fokus ist jetzt **rein gerätelokal**: Broadcasts aktualisieren nur Namen + Typ, wechseln aber nicht mehr das Projekt. Umschalten geht ausschließlich lokal über den Projekt-Drawer. So arbeitet jedes Gerät unabhängig in seinem eigenen Projekt (auch mehrere App-Instanzen).
- Hinweis: „ARIA, geh in Projekt X" per Sprache wechselt das App-Projekt dadurch **nicht mehr** automatisch — bewusst, damit Geräte unabhängig bleiben. Wechseln über den Drawer.
### Deploy
`git pull && docker compose up -d --build brain` + APK 0.2.2.1 neu bauen.
---
## [0.2.2.0] — 2026-07-17 — Workbench: Taskleisten-Dock statt Zoom-Landkarte + bedienbare VNC-Konsole
### Geändert
+1 -1
View File
@@ -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",
+33 -4
View File
@@ -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<Set<string>>(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,10 +1009,18 @@ const ChatScreen: React.FC = () => {
if (p.id && (p.kind === 'code' || p.kind === 'chat')) {
projectFocus.setKind(p.id, p.kind);
}
if (action === 'entered' || action === 'created') {
if (p.id) setFocusedProjectId(p.id);
} else if (action === 'exited') {
setFocusedProjectId('');
// 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;
}
@@ -1620,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();
@@ -1723,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();
@@ -1799,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({
@@ -2202,6 +2230,7 @@ const ChatScreen: React.FC = () => {
// ueber audioRequestId-Match nachgereicht wenn whisper das Endpoint feuert.
const handleVoiceButtonStart = useCallback(async (): Promise<boolean> => {
const audioRequestId = `audio_${Date.now()}_${Math.floor(Math.random() * 100000)}`;
rememberMyRequest(audioRequestId);
const wasInterrupted = interruptAriaIfBusy();
const location = await getCurrentLocation();
+15 -8
View File
@@ -1515,7 +1515,7 @@ class Agent:
targs = {}
logger.info("[router] lokal Tool-Call: %s(%s)", tname,
", ".join(targs.keys()))
tresult = self._dispatch_tool(tname, targs)
tresult = self._dispatch_tool(tname, targs, active_project_id)
# Echter Skill (run_*) erfolgreich? Dann uebernimmt dieser
# Turn das speak-Flag des Skills (Steuerbefehl=stumm,
# Antwort-Skill=vorlesen). Letzter Skill gewinnt.
@@ -1807,7 +1807,7 @@ class Agent:
))
# Tools ausfuehren + Ergebnis als role=tool zurueck
for tc in result.tool_calls:
tool_result = self._dispatch_tool(tc["name"], tc["arguments"])
tool_result = self._dispatch_tool(tc["name"], tc["arguments"], active_project_id)
# Steuerbefehl-Skill (run_*, speak=false) erfolgreich
# ausgefuehrt → Antwort nicht vorlesen (letzter Skill gewinnt).
_tn = tc.get("name") or ""
@@ -1895,10 +1895,12 @@ class Agent:
# ── Tool-Dispatcher ───────────────────────────────────────
def _dispatch_tool(self, name: str, arguments: dict) -> str:
def _dispatch_tool(self, name: str, arguments: dict, project_id: str = "") -> str:
"""Fuehrt einen Tool-Call aus und gibt ein kurzes Text-Resultat zurueck.
Niemals werfen — Fehler werden als Text-Resultat reportet damit Claude
weitermachen kann."""
weitermachen kann. project_id = Projekt DIESES Requests (fuer Tools die
aufs aktuelle Projekt wirken, z.B. set_project_kind) — NICHT der globale
active_project (geraeteuebergreifend)."""
try:
if name == "skill_create":
# ARIA-Skills sind immer Python — execution ist nicht mehr im Schema
@@ -2602,13 +2604,18 @@ class Agent:
kind = (arguments.get("kind") or "").strip().lower()
if kind not in ("code", "chat"):
return "FEHLER: kind muss 'code' oder 'chat' sein."
active_id = projects_mod.get_active()
# WICHTIG: das Projekt des AKTUELLEN Requests markieren, NICHT den
# globalen active_project — der ist geraeteuebergreifend und wuerde
# (bei mehreren App-/Diagnostic-Instanzen) das falsche Projekt
# treffen. project_id kommt pro Request vom Client.
active_id = (project_id or "").strip()
if not active_id:
return ("Kein aktives Projekt — Du bist im Hauptthread. Erst ein Projekt "
"anlegen/betreten (project_create/project_enter), dann set_project_kind.")
return ("Kein aktives Projekt — dieser Chat laeuft im Hauptthread. "
"set_project_kind wirkt nur, wenn der Request in einem Projekt "
"laeuft (der Client sendet die project_id mit).")
updated = projects_mod.update_project(active_id, {"kind": kind})
if not updated:
return f"FEHLER: aktives Projekt '{active_id}' nicht gefunden."
return f"FEHLER: Projekt '{active_id}' nicht gefunden."
self._pending_events.append({
"type": "project_changed",
"project": updated,
+10 -2
View File
@@ -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: