fix+feat(projects): Spinner-Bug, Back-Button, kollabierbare Chat-Bloecke, File-Filter
Drei Stefan-Bugs aus dem ersten Deploy-Test plus die fehlenden Polish-
Features fuer die Projekt-Funktion.
Fixes:
- ProjectsBrowser-Spinner-Hang: useRef-Pattern statt useCallback([onActive
Changed]) — Parent uebergibt inline-arrow-Callbacks, neue Identitaet
jedes Render → useCallback recomputes → useEffect refeuert → infinite
Spinner. Fix: Ref-Bridge fuer Callbacks, useCallback mit empty deps.
- ChatScreen Banner: zusaetzlicher × Hauptchat-Button rechts (sichtbar
nur wenn Projekt aktiv) — ein Tap und zurueck zum Hauptthread, ohne
Modal-Umweg.
Features:
- Brain ChatOut.project_id: aktive Projekt-ID NACH dem Turn (kann
durch project_enter/exit-Tools waehrend Turn gewechselt sein). Bridge
liest sie aus dem /chat-Response und haengt sie an jeden ARIA-Chat-
Broadcast als payload.projectId.
- App: ChatMessage.projectId-Feld. User-Bubbles werden mit aktiver
Projekt-ID getaggt vor dem Senden (auch im RVS-Payload). ARIA-Bubbles
kriegen die ID vom Bridge.
- App: Chat-Verlauf rendert aufeinanderfolgende Project-Messages als
einklappbaren Block mit Header (▶/▼ + Projekt-Name + Count). Auto-
Collapse beim Projekt-Wechsel (altes ein, neues aus), Default beim
ersten Render: alle inaktiven Projekte eingeklappt.
- File-Manager Project-Tagging:
- diagnostic/server.js: Manifest /shared/config/file_projects.json
+ /api/files-list returnt projectId pro Datei + neuer Endpoint
/api/files-set-project.
- bridge/aria_bridge.py: nach App-Upload Auto-Tag mit aktivem Projekt
(Brain-Status-Query, best-effort fail-silent).
- App SettingsScreen: scrollbare Projekt-Pill-Reihe als Filter, default
auf aktives Projekt wenn vorhanden, sonst "Alle Projekte".
- Diagnostic: zweites Dropdown im Files-Tab, baut Projekt-Optionen
dynamisch aus /api/brain/projects/list.
Bewusst nicht drin (Folgeschritt):
- Per-File "Projekt zuweisen"-Action (Long-Press / Right-Click)
- Filter-Sync zwischen ChatScreen-Banner und SettingsScreen-Filter
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+44
-1
@@ -1005,6 +1005,37 @@ class ARIABridge:
|
||||
cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
|
||||
return cleaned, files, missing
|
||||
|
||||
def _tag_file_to_active_project(self, file_path: str) -> None:
|
||||
"""Holt vom Brain das aktive Projekt + schreibt file_path → project_id
|
||||
in /shared/config/file_projects.json. Best-effort, fail-silent.
|
||||
Wird vom File-Save-Handler nach erfolgreichem Schreiben aufgerufen."""
|
||||
try:
|
||||
brain_url = os.environ.get("BRAIN_URL", "http://aria-brain:8080")
|
||||
with urllib.request.urlopen(f"{brain_url}/projects/status", timeout=5) as r:
|
||||
data = json.loads(r.read())
|
||||
active_id = (data.get("active_id") or "").strip()
|
||||
if not active_id:
|
||||
return
|
||||
manifest_path = "/shared/config/file_projects.json"
|
||||
os.makedirs("/shared/config", exist_ok=True)
|
||||
try:
|
||||
with open(manifest_path) as f:
|
||||
manifest = json.load(f)
|
||||
if not isinstance(manifest, dict):
|
||||
manifest = {}
|
||||
except FileNotFoundError:
|
||||
manifest = {}
|
||||
except Exception:
|
||||
manifest = {}
|
||||
manifest[file_path] = active_id
|
||||
tmp = manifest_path + ".tmp"
|
||||
with open(tmp, "w") as f:
|
||||
json.dump(manifest, f, indent=2, ensure_ascii=False)
|
||||
os.replace(tmp, manifest_path)
|
||||
logger.info("[file-project] %s → %s", file_path, active_id)
|
||||
except Exception as exc:
|
||||
logger.warning("[file-project] tag failed (%s): %s", file_path, exc)
|
||||
|
||||
async def _broadcast_aria_file(self, file_info: dict) -> None:
|
||||
"""ARIA hat eine Datei fuer den User erstellt — App+Diagnostic informieren."""
|
||||
logger.info("[rvs] ARIA-Datei rausgeben: %s (%s, %dKB)",
|
||||
@@ -1224,6 +1255,9 @@ class ARIABridge:
|
||||
"backupTs": assistant_backup_ts,
|
||||
# Debug: aufbereiteter Text fuer TTS (App ignoriert, Diagnostic zeigt optional)
|
||||
"ttsText": tts_text_preview if tts_text_preview != text else "",
|
||||
# Projekt-Zuordnung — App + Diagnostic sortieren die Bubble in
|
||||
# den passenden Projekt-Block. Leer = Hauptchat.
|
||||
"projectId": (payload.get("projectId") or "") if isinstance(payload, dict) else "",
|
||||
},
|
||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||
})
|
||||
@@ -1521,6 +1555,11 @@ class ARIABridge:
|
||||
await self._emit_activity("idle", "")
|
||||
return
|
||||
|
||||
# Projekt-Kontext des Turns — wird an _process_core_response weiter-
|
||||
# gegeben damit der chat-Broadcast die Bubble dem richtigen Projekt-
|
||||
# Block in App + Diagnostic zuordnen kann.
|
||||
turn_project_id = (data.get("project_id") or "").strip()
|
||||
|
||||
# Side-Channel-Events VOR der Chat-Bubble broadcasten (z.B. skill_created)
|
||||
# damit sie in der UI vor der Reply auftauchen
|
||||
for event in data.get("events", []) or []:
|
||||
@@ -1586,7 +1625,7 @@ class ARIABridge:
|
||||
# passend behandelt wird (hier minimal, weil Brain noch keine
|
||||
# metadata mitschickt).
|
||||
try:
|
||||
await self._process_core_response(reply, {})
|
||||
await self._process_core_response(reply, {"projectId": turn_project_id})
|
||||
except Exception:
|
||||
logger.exception("[brain] _process_core_response Fehler")
|
||||
await self._emit_activity("idle", "")
|
||||
@@ -2125,6 +2164,10 @@ class ARIABridge:
|
||||
f.write(base64.b64decode(file_b64))
|
||||
size_kb = len(file_b64) // 1365
|
||||
logger.info("[rvs] Datei gespeichert: %s (%dKB)", file_path, size_kb)
|
||||
# Datei dem aktuellen Projekt zuordnen (falls Stefan in einem ist).
|
||||
# Manifest in /shared/config/file_projects.json — File-Manager
|
||||
# in App + Diagnostic filtert danach.
|
||||
self._tag_file_to_active_project(file_path)
|
||||
|
||||
# Pixel-Bilder fuer Claude-Vision shrinken wenn > 2 MB. SVG/PDF/ZIP
|
||||
# bleiben unangetastet (Vision laeuft eh nur auf Raster-Formaten).
|
||||
|
||||
Reference in New Issue
Block a user