feat: Workspace-Umbau Schritt 5 — Live-Code-Editor + code_file-Strom

Durchgaengiger Live-Editor fuer Code-Projekte:
- App: selbstenthaltener Highlight-Editor in einer WebView (editorHtml.ts,
  Textarea + Regex-Highlight-Layer, voll offline). CodeEditorTile mit Datei-Tabs,
  verdrahtet mit dem codeFile-Spiegel; Bridge-Protokoll setContent/applyPatch/
  setReadOnly ↔ onEditFromUser.
- Proxy-Hook (routes.js): faengt ARIAs Write/Edit/MultiEdit unter
  /shared/projects/<pid>/ ab und postet den Volltext bei Erfolg an
  /internal/code-file. tool_use_id→file_path-Korrelation, liest die Datei aus
  dem gemounteten /shared.
- Bridge: /internal/code-file relayt als RVS code_file an die App; eingehende
  code_file_edit schreiben den Volltext pfad-sicher nach /shared/projects/<pid>/
  (_write_project_file, kein Ausbruch via ..).

Arbeitsverzeichnis fuer Code-Projekte = /shared/projects/<projectId>/ (in proxy/
bridge/brain gemountet, kein SSH noetig). tsc/py/js clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 23:53:48 +02:00
co-authored by Claude Opus 4.8
parent 85363b1014
commit a6cb152f55
4 changed files with 388 additions and 12 deletions
+62
View File
@@ -3351,6 +3351,19 @@ class ARIABridge:
logger.info("[rvs] flux-bridge -> %s", state)
return
elif msg_type == "code_file_edit":
# Stefan hat im Live-Editor getippt → Datei im Projekt-Arbeits-
# verzeichnis (/shared/projects/<pid>/) mit dem Volltext ueberschreiben.
# ARIA sieht die Aenderung beim naechsten Read.
pid = str(payload.get("projectId") or "")
rel = (payload.get("path") or "").strip()
full_text = payload.get("fullText")
if rel and isinstance(full_text, str):
ok = self._write_project_file(pid, rel, full_text)
logger.info("[rvs] code_file_edit %s/%s (%d chars) → %s",
pid or "main", rel, len(full_text), "ok" if ok else "abgelehnt")
return
elif msg_type == "config_request":
# Eine andere Bridge (whisper/f5tts) bittet um die aktuelle Voice-
# Config — passiert wenn sie sich connected, weil sie sonst die
@@ -4192,6 +4205,33 @@ class ARIABridge:
"timestamp": int(time.time() * 1000),
}))
await _send_response(writer, 200, {"ok": True})
elif method == "POST" and path == "/internal/code-file":
# Vom Proxy-Hook gefeuert wenn ARIA in einem Code-Projekt eine
# Datei schreibt/aendert. Wir spiegeln das als RVS code_file an
# die App (Live-Code-Editor). payload: {projectId, path,
# language?, content?, patch?, version?}.
try:
data = json.loads(body.decode("utf-8", "ignore"))
except Exception as exc:
await _send_response(writer, 400, {"error": f"bad json: {exc}"})
return
fpath = (data.get("path") or "").strip()
if not fpath:
await _send_response(writer, 400, {"error": "path erforderlich"})
return
asyncio.create_task(self._send_to_rvs({
"type": "code_file",
"payload": {
"projectId": str(data.get("projectId") or ""),
"path": fpath,
"language": data.get("language") or "",
"content": data.get("content"),
"patch": data.get("patch"),
"version": data.get("version") or 0,
},
"timestamp": int(time.time() * 1000),
}))
await _send_response(writer, 200, {"ok": True})
elif method == "POST" and path == "/internal/flux-generate":
# Vom Brain (flux_generate-Tool) gefeuert. Wir routen den
# Render-Request via RVS an die flux-bridge (Gamebox),
@@ -4301,6 +4341,28 @@ class ARIABridge:
except Exception:
logger.exception("[bridge] Internal HTTP-Listener konnte nicht starten")
def _write_project_file(self, project_id: str, rel_path: str, content: str) -> bool:
"""Schreibt content nach /shared/projects/<project_id>/<rel_path>.
Pfad-sicher: rel_path darf nicht aus dem Projekt-Basisverzeichnis
ausbrechen (kein .., kein absoluter Pfad). Legt Verzeichnisse an.
"""
try:
base = Path("/shared/projects") / (project_id or "main")
base = base.resolve()
target = (base / rel_path).resolve()
if base != target and base not in target.parents:
logger.warning("[code_file] Pfad ausserhalb Projekt abgelehnt: %s", rel_path)
return False
target.parent.mkdir(parents=True, exist_ok=True)
tmp = target.with_suffix(target.suffix + ".tmp")
tmp.write_text(content, encoding="utf-8")
tmp.replace(target)
return True
except Exception as exc:
logger.warning("[code_file] Schreiben fehlgeschlagen %s/%s: %s", project_id, rel_path, exc)
return False
async def _delete_chat_message(self, ts: int) -> dict:
"""Entfernt eine Bubble: aus chat_backup.jsonl + Brain conversation,
broadcastet chat_message_deleted via RVS.