feat: Modell pro Chat waehlen — und sehen, welches gerade laeuft

Nach einem Sicherheitsfilter-Treffer fuhr ein Projekt still auf dem
Ausweich-Modell weiter, und nirgends war zu sehen, auf welchem. Zurueck ging
nur ueber "Liste leeren" im Filter-Panel, und das gleich fuer alle Chats.

Jetzt drei Ebenen (aria-brain/context_model.py):
  Einstellung (brainModel) < pro Chat < Ausweich (Sicherheitsfilter)

- Pro Chat: Hauptchat und jedes Projekt koennen ein eigenes Modell haben, in
  /shared/config/context_models.json. Bewusst kein Feld im Projekt-Manifest —
  der Hauptchat ist kein Projekt und waere sonst aussen vor.
- "↩ zurueck" pro Chat: loescht nur die Filter-Vermerke DIESES Chats. Blockt
  der Filter erneut, weicht das Brain eben wieder aus.
- Angezeigt wird, was wirklich laeuft: chat() gibt das Modell aus modelUsage
  der CLI zurueck (aufgeloeste Version statt Alias) samt Grund; Brain → Bridge
  → chat_backup + Live-Payload → App und Diagnostic. Als Grundeinstellung
  zaehlt das Modell, mit dem das Brain laeuft, nicht der Dateiwert — so faellt
  ein ausstehender Neustart nach brainModel-Aenderung auf.

Diagnostic: 🧠-Auswahl neben "+ Projekt" fuer den fokussierten Chat, Modell im
Untertitel jeder Kontext-Karte, Modell statt "Claude" am Bubble-Badge (orange
mit ↪ bei Ausweich).

App: hinter der Quell-Anzeige (Einstellungen, Default aus — Kopfzeile bleibt
fuer Mama unveraendert) ein Modell-Chip in der Kopfzeile; Tippen oeffnet die
Auswahl fuer diesen Chat inkl. "Zurueck". Laeuft ueber den vorhandenen
brain_request-Kanal, also keine neuen RVS-Typen.

Tests: 47/47 (12 neue fuer die Aufloesungsreihenfolge gegen agent.chat()),
tsc ohne Fehler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 13:39:30 +02:00
co-authored by Claude Opus 5.5
parent d221c1a320
commit 6d978d72b3
9 changed files with 596 additions and 19 deletions
+20 -2
View File
@@ -1593,14 +1593,22 @@ class ARIABridge:
# File-Marker werden separat als file_from_aria-Events ausgeliefert.
display_text = strip_voice_tag_for_display(text)
_answered_by = (payload.get("answeredBy") or "") if isinstance(payload, dict) else ""
assistant_backup_ts = self._append_chat_backup({
_model = (payload.get("model") or "") if isinstance(payload, dict) else ""
_model_reason = (payload.get("modelReason") or "") if isinstance(payload, dict) else ""
_backup_rec = {
"role": "assistant",
"text": display_text,
"files": [{"serverPath": f["serverPath"], "name": f["name"],
"mimeType": f["mimeType"], "size": f["size"]} for f in aria_files],
"project_id": turn_pid,
"answeredBy": _answered_by,
})
}
# Nur wenn ein Claude-Modell lief — sonst bliebe ein leeres Feld in
# jeder Fast-Path-Zeile stehen.
if _model:
_backup_rec["model"] = _model
_backup_rec["modelReason"] = _model_reason
assistant_backup_ts = self._append_chat_backup(_backup_rec)
metadata = payload.get("metadata", {})
is_critical = metadata.get("critical", False)
@@ -1651,6 +1659,10 @@ class ARIABridge:
"projectId": (payload.get("projectId") or "") if isinstance(payload, dict) else "",
# Quell-Backend (local/claude/fast-path) fuer den Diagnostic-Badge.
"answeredBy": (payload.get("answeredBy") or "") if isinstance(payload, dict) else "",
# Welches Claude-Modell + warum ("einstellung"/"pro-chat"/
# "ausweich"). Leer bei Fast-Path/lokal.
"model": _model,
"modelReason": _model_reason,
# Wird diese Antwort vorgelesen? Fast-Path/Steuerbefehl = False.
# Die App braucht das: ohne TTS feuert onPlaybackFinished nie,
# und daran haengt das Wake-Word-Re-Arm — sonst bleibt das Ohr
@@ -2079,6 +2091,10 @@ class ARIABridge:
# Welcher Backend geantwortet hat (local/claude/fast-path) — fuer den
# Quell-Badge in Diagnostic.
answered_by = (data.get("answered_by") or "claude").strip()
# Welches Claude-Modell geantwortet hat + warum (einstellung/pro-chat/
# ausweich). Fuer die Anzeige in App und Diagnostic.
answered_model = (data.get("model") or "").strip()
model_reason = (data.get("model_reason") or "").strip()
# Soll vorgelesen werden? Fast-Path (Steuerbefehl) = False. System-Flag
# vom Brain — robust, unabhaengig von <voice>-Tags im Reply-Text.
speak = data.get("speak", True)
@@ -2181,6 +2197,8 @@ class ARIABridge:
try:
await self._process_core_response(reply, {"projectId": turn_project_id,
"answeredBy": answered_by,
"model": answered_model,
"modelReason": model_reason,
"speak": speak,
"converse": converse,
"awaiting_reply": awaiting_reply,