feat: Editor laedt vorhandene Dateien + manueller Code-Toggle (App + Diagnostic)

Editor zeigte "keine Datei", obwohl ARIA schon Dateien geschrieben hatte — er las
NUR den Live-code_file-Stream, nie den Bestand. Jetzt:
- Brain: GET /projects/<id>/files + /file (liest /shared/projects/<id>/, pfad-sicher,
  512KB-Cap). kind in ProjectUpdateBody (PATCH akzeptiert 'code'|'chat').
- App: brainApi.listProjectFiles/readProjectFile/setProjectKind. CodeEditorTile
  holt beim Oeffnen die vorhandene Dateiliste + laedt Inhalt (Live-Version hat
  Vorrang). ProjectsBrowser-Edit: Code-Projekt-Toggle (spiegelt sofort in
  projectFocus → Cockpit-Panels).
- Diagnostic: </> Code-Toggle je Projektzeile + Code-Badge.

py/node/tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 23:20:39 +02:00
co-authored by Claude Opus 4.8
parent 9dd7a2cb14
commit 5fa5d79ad8
5 changed files with 182 additions and 30 deletions
+65 -25
View File
@@ -1,57 +1,97 @@
/**
* CodeEditorTile — Live-Code-Editor (WebView, editorHtml.ts).
*
* Zeigt live, was ARIA in diesem Projekt schreibt (aus dem codeFile-Spiegel)
* und laesst Stefan selbst editieren — Aenderungen gehen als code_file_edit
* zurueck an die Bridge. Datei-Tabs oben zum Umschalten.
* Zeigt die Dateien eines Code-Projekts aus /shared/projects/<id>/:
* - beim Oeffnen werden die BEREITS vorhandenen Dateien vom Brain geladen
* (listProjectFiles/readProjectFile) — sonst waere der Editor leer, obwohl
* ARIA schon Dateien geschrieben hat.
* - live schreibt ARIA weiter → code_file-Stream aktualisiert die offene Datei.
* Stefan kann selbst editieren → code_file_edit zurueck an die Bridge.
*/
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { WebView, WebViewMessageEvent } from 'react-native-webview';
import codeFile, { CodeFileState } from '../../services/codeFile';
import codeFile from '../../services/codeFile';
import brainApi from '../../services/brainApi';
import { EDITOR_HTML } from '../assets/editorHtml';
interface Props {
projectId: string;
}
function guessLang(path: string): string {
const ext = (path.split('.').pop() || '').toLowerCase();
const map: Record<string, string> = {
js: 'javascript', ts: 'typescript', tsx: 'typescript', py: 'python',
c: 'c', h: 'c', cpp: 'cpp', asm: 'asm', s: 'asm', sh: 'shell', bash: 'shell',
html: 'html', css: 'css', json: 'json', yaml: 'yaml', yml: 'yaml', md: 'markdown',
go: 'go', rs: 'rust', java: 'java', kt: 'kotlin', txt: 'text',
};
return map[ext] || 'text';
}
const CodeEditorTile: React.FC<Props> = ({ projectId }) => {
const webRef = useRef<WebView>(null);
const [files, setFiles] = useState<CodeFileState[]>(() => codeFile.getFiles(projectId));
const [currentPath, setCurrentPath] = useState<string | null>(files[0]?.path ?? null);
// Pfade aus dem Brain (vorhandene Dateien) — mit Live-Dateien gemergt.
const [serverPaths, setServerPaths] = useState<string[]>([]);
const [currentPath, setCurrentPath] = useState<string | null>(null);
const [loadErr, setLoadErr] = useState<string>('');
const readyRef = useRef(false);
const currentPathRef = useRef<string | null>(currentPath);
currentPathRef.current = currentPath;
// Vereinigte, sortierte Dateiliste (Live-Spiegel + Server-Dateien).
const files = useMemo(() => {
const set = new Set<string>(serverPaths);
for (const f of codeFile.getFiles(projectId)) set.add(f.path);
return Array.from(set).sort((a, b) => a.localeCompare(b));
}, [serverPaths, projectId]);
const sendToWeb = useCallback((payload: Record<string, unknown>) => {
const js = `window.ariaBridge && window.ariaBridge.onMessage(${JSON.stringify(JSON.stringify(payload))}); true;`;
webRef.current?.injectJavaScript(js);
}, []);
const loadFileIntoEditor = useCallback((path: string | null) => {
const loadFileIntoEditor = useCallback(async (path: string | null) => {
if (!path) { sendToWeb({ cmd: 'setContent', content: '', language: 'text', version: 0 }); return; }
const f = codeFile.getFile(projectId, path);
sendToWeb({ cmd: 'setContent', content: f?.content ?? '', language: f?.language ?? 'text', version: f?.version ?? 0 });
// Live-Version bevorzugen (falls ARIA gerade schreibt), sonst vom Brain holen.
const live = codeFile.getFile(projectId, path);
if (live) {
sendToWeb({ cmd: 'setContent', content: live.content, language: live.language, version: live.version });
return;
}
try {
const res = await brainApi.readProjectFile(projectId, path);
sendToWeb({ cmd: 'setContent', content: res.content ?? '', language: guessLang(path), version: 0 });
} catch (e: any) {
sendToWeb({ cmd: 'setContent', content: `// Konnte ${path} nicht laden: ${e?.message || e}`, language: 'text', version: 0 });
}
}, [projectId, sendToWeb]);
// Projektwechsel: Dateiliste + Auswahl neu.
// Projektwechsel: vorhandene Dateien vom Brain laden.
useEffect(() => {
const list = codeFile.getFiles(projectId);
setFiles(list);
setCurrentPath((prev) => (prev && list.some((f) => f.path === prev) ? prev : list[0]?.path ?? null));
let cancelled = false;
setLoadErr('');
brainApi.listProjectFiles(projectId)
.then(res => {
if (cancelled) return;
const paths = (res.files || []).map(f => f.path);
setServerPaths(paths);
setCurrentPath(prev => (prev && paths.includes(prev)) ? prev : (paths[0] ?? codeFile.getFiles(projectId)[0]?.path ?? null));
})
.catch(e => { if (!cancelled) setLoadErr(String(e?.message || e)); });
return () => { cancelled = true; };
}, [projectId]);
// Eingehende Updates aus dem Spiegel.
// Live-Updates aus dem Spiegel.
useEffect(() => {
return codeFile.subscribe((u) => {
if ((u.projectId || '') !== (projectId || '')) return;
setFiles(codeFile.getFiles(projectId));
// Noch keine Datei gewaehlt → diese oeffnen.
setServerPaths(prev => prev.includes(u.path) ? prev : [...prev, u.path]);
if (!currentPathRef.current) { setCurrentPath(u.path); return; }
if (u.path !== currentPathRef.current) return;
if (!readyRef.current) return;
if (u.path !== currentPathRef.current || !readyRef.current) return;
if (u.patch) {
sendToWeb({ cmd: 'applyPatch', from: u.patch.from, to: u.patch.to, insert: u.patch.insert, version: u.version });
} else {
@@ -60,7 +100,7 @@ const CodeEditorTile: React.FC<Props> = ({ projectId }) => {
});
}, [projectId, sendToWeb]);
// Datei-Auswahl gewechselt → in den Editor laden (falls WebView bereit).
// Datei-Auswahl gewechselt → laden (falls WebView bereit).
useEffect(() => {
if (readyRef.current) loadFileIntoEditor(currentPath);
}, [currentPath, loadFileIntoEditor]);
@@ -82,14 +122,14 @@ const CodeEditorTile: React.FC<Props> = ({ projectId }) => {
<View style={styles.container}>
<View style={styles.tabsRow}>
{files.length === 0 ? (
<Text style={styles.noFiles}>Noch keine Datei</Text>
<Text style={styles.noFiles}>{loadErr ? `Fehler: ${loadErr}` : 'Noch keine Datei in diesem Projekt'}</Text>
) : (
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.tabs}>
{files.map((f) => {
const active = f.path === currentPath;
const name = f.path.split('/').pop() || f.path;
{files.map((path) => {
const active = path === currentPath;
const name = path.split('/').pop() || path;
return (
<TouchableOpacity key={f.path} onPress={() => setCurrentPath(f.path)} style={[styles.tab, active && styles.tabActive]}>
<TouchableOpacity key={path} onPress={() => setCurrentPath(path)} style={[styles.tab, active && styles.tabActive]}>
<Text style={[styles.tabText, active && styles.tabTextActive]} numberOfLines={1}>{name}</Text>
</TouchableOpacity>
);