fix(projects): Anhaenge landen im gewaehlten Projekt statt im Hauptchat

Bug: Bild/Datei ins Textfeld + Frage → beides landete im Hauptchat, egal
welches Projekt fokussiert war. Der Anhang-Pfad reichte die projectId nirgends
durch (im Gegensatz zum reinen Text-Pfad).

App (sendPendingAttachments):
- lokale Anhang-Bubble bekommt projectId (App-Focus)
- file-Upload (rvs.send('file')) schickt projectId mit
- Merge-Trigger chat-Nachricht schickt projectId mit

Bridge:
- file-Handler liest payload.projectId, merkt sie (_pending_files_project_id)
  und taggt die Datei per _tag_file_to_project ins richtige Projekt
- _flush_pending_files_with_text(user_text, project_id): reicht die projectId
  aus dem chat-Payload an send_to_core (Fallback: gemerkter Upload-Kontext)
- _flush_pending_files_after (Files-only): nutzt den gemerkten Upload-Kontext
- merged-Aufruf im chat-Handler gibt payload.projectId mit

Damit tragen User-Bubble, Datei-Manifest, Brain-Turn und ARIA-Antwort alle
denselben Projekt-Tag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-06 20:38:47 +02:00
parent 5a7bfd9f50
commit 596d0bb243
2 changed files with 40 additions and 12 deletions
+36 -12
View File
@@ -658,6 +658,10 @@ class ARIABridge:
# Liste von Tuples: (file_path, name, file_type, size_kb, width, height)
self._pending_files: list[tuple[str, str, str, int, int, int]] = []
self._pending_files_flush_task: Optional[asyncio.Task] = None
# Projekt-Kontext der gerade gepufferten Anhaenge (aus dem file-Upload).
# Wird beim Flush an send_to_core gegeben, damit Anhaenge im richtigen
# Projekt landen statt im Hauptchat.
self._pending_files_project_id: str = ""
self._PENDING_FILES_WINDOW_SEC: float = 0.8
def initialize(self) -> None:
@@ -1508,12 +1512,19 @@ class ARIABridge:
text = self._build_pending_files_message("")
self._pending_files = []
self._pending_files_flush_task = None
await self.send_to_core(text, source="app-file")
pid = self._pending_files_project_id
self._pending_files_project_id = ""
await self.send_to_core(text, source="app-file", project_id=pid)
async def _flush_pending_files_with_text(self, user_text: str) -> bool:
async def _flush_pending_files_with_text(self, user_text: str,
project_id: str = "") -> bool:
"""Wenn ein chat-Text reinkommt waehrend Files gepuffert sind:
Files + Text zu einer einzigen aria-core-Nachricht mergen.
Returns True wenn gemerged wurde (Caller soll dann nicht nochmal senden)."""
Returns True wenn gemerged wurde (Caller soll dann nicht nochmal senden).
project_id: Projekt-Kontext aus dem chat-Payload (der sichtbare Focus
beim Absenden). Faellt auf den beim File-Upload gemerkten Kontext
zurueck, damit Anhaenge im richtigen Projekt landen statt im Hauptchat."""
if not self._pending_files:
return False
if self._pending_files_flush_task and not self._pending_files_flush_task.done():
@@ -1521,9 +1532,11 @@ class ARIABridge:
self._pending_files_flush_task = None
text = self._build_pending_files_message(user_text)
self._pending_files = []
pid = (project_id or "").strip() or self._pending_files_project_id
self._pending_files_project_id = ""
# create_task statt await — sonst blockt der RVS-recv-Loop bis Brain
# fertig ist (siehe chat-handler oben).
asyncio.create_task(self.send_to_core(text, source="app-file+chat"))
asyncio.create_task(self.send_to_core(text, source="app-file+chat", project_id=pid))
return True
async def send_to_core(self, text: str, source: str = "bridge",
@@ -1965,9 +1978,11 @@ class ARIABridge:
# Wenn Files gerade gepuffert sind (Bild + Text gleichzeitig
# gesendet), mergen wir sie zu einer einzigen Anfrage statt
# zwei separater send_to_core-Calls.
merged = await self._flush_pending_files_with_text(text)
merged = await self._flush_pending_files_with_text(
text, project_id=str(payload.get("projectId") or ""))
if merged:
logger.info("[rvs] App-Chat (mit Anhaengen): '%s'", text[:80])
logger.info("[rvs] App-Chat (mit Anhaengen) project=%s: '%s'",
str(payload.get("projectId") or "") or "(main)", text[:80])
else:
core_text = self._build_core_text(text, interrupted, location)
logger.info("[rvs] App-Chat%s%s: '%s'",
@@ -2189,8 +2204,14 @@ class ARIABridge:
file_b64 = payload.get("base64", "")
width = payload.get("width", 0)
height = payload.get("height", 0)
logger.info("[rvs] Datei empfangen: %s (%s, %dKB)",
file_name, file_type, len(file_b64) // 1365 if file_b64 else 0)
# Projekt-Kontext des Uploads (sichtbarer App-Focus). Merken, damit
# der spaetere Flush (Files+Text oder Files-only) die Anfrage im
# richtigen Projekt an das Brain schickt statt im Hauptchat.
file_project_id = str(payload.get("projectId") or "")
self._pending_files_project_id = file_project_id
logger.info("[rvs] Datei empfangen: %s (%s, %dKB) project=%s",
file_name, file_type, len(file_b64) // 1365 if file_b64 else 0,
file_project_id or "(main)")
SHARED_DIR = "/shared/uploads"
os.makedirs(SHARED_DIR, exist_ok=True)
@@ -2198,7 +2219,8 @@ class ARIABridge:
if not file_b64:
text = f"Stefan hat eine Datei gesendet ({file_name}, {file_type}) aber die Daten sind leer angekommen."
# create_task statt await — RVS-recv darf nicht blocken
asyncio.create_task(self.send_to_core(text, source="app-file"))
asyncio.create_task(self.send_to_core(text, source="app-file",
project_id=file_project_id))
return
if file_type.startswith("image/"):
@@ -2212,10 +2234,12 @@ 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).
# Datei dem Projekt des Uploads zuordnen (Multi-Threading: explizit
# aus dem file-Payload, kein globaler active_project-State mehr).
# Manifest in /shared/config/file_projects.json — File-Manager
# in App + Diagnostic filtert danach.
self._tag_file_to_active_project(file_path)
# in App + Diagnostic filtert danach. Leer = Hauptchat.
if file_project_id:
self._tag_file_to_project(file_path, file_project_id)
# Pixel-Bilder fuer Claude-Vision shrinken wenn > 2 MB. SVG/PDF/ZIP
# bleiben unangetastet (Vision laeuft eh nur auf Raster-Formaten).