From 5fa5d79ad8ad4e056b2e235ff44cbb20172f6895 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Mon, 20 Jul 2026 23:20:39 +0200 Subject: [PATCH] feat: Editor laedt vorhandene Dateien + manueller Code-Toggle (App + Diagnostic) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//files + /file (liest /shared/projects//, 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 --- android/src/components/ProjectsBrowser.tsx | 30 ++++++- android/src/services/brainApi.ts | 22 ++++- .../src/workspace/tiles/CodeEditorTile.tsx | 90 +++++++++++++------ aria-brain/main.py | 56 ++++++++++++ diagnostic/index.html | 14 +++ 5 files changed, 182 insertions(+), 30 deletions(-) diff --git a/android/src/components/ProjectsBrowser.tsx b/android/src/components/ProjectsBrowser.tsx index aaa3549..311189f 100644 --- a/android/src/components/ProjectsBrowser.tsx +++ b/android/src/components/ProjectsBrowser.tsx @@ -28,6 +28,7 @@ import { import brainApi, { Project } from '../services/brainApi'; import rvs from '../services/rvs'; +import projectFocus from '../services/projectFocus'; interface Props { /** Optional โ€” wenn als Modal genutzt, sonst inline */ @@ -75,6 +76,7 @@ export const ProjectsBrowser: React.FC = ({ visible = true, onClose, onAc const [editing, setEditing] = useState(null); const [editName, setEditName] = useState(''); const [editDesc, setEditDesc] = useState(''); + const [editKind, setEditKind] = useState<'code' | 'chat'>('chat'); // Versteckte Projekte standardmaessig ausblenden; Toggle blendet sie // temporaer (gedimmt) ein โ€” zum Ansehen/Auswaehlen oder Wieder-Sichtbarmachen. const [showHidden, setShowHidden] = useState(false); @@ -148,18 +150,25 @@ export const ProjectsBrowser: React.FC = ({ visible = true, onClose, onAc setEditing(p); setEditName(p.name); setEditDesc(p.description || ''); + setEditKind(p.kind === 'code' ? 'code' : 'chat'); }, []); const saveEdit = useCallback(() => { if (!editing) return; - const patch: Partial> = {}; + const patch: Partial> = {}; if (editName.trim() && editName.trim() !== editing.name) patch.name = editName.trim(); if (editDesc.trim() !== (editing.description || '')) patch.description = editDesc.trim(); + const curKind = editing.kind === 'code' ? 'code' : 'chat'; + if (editKind !== curKind) patch.kind = editKind; if (Object.keys(patch).length === 0) { setEditing(null); return; } brainApi.updateProject(editing.id, patch) - .then(() => { setEditing(null); load(); }) + .then(() => { + // Kind sofort in den Workspace spiegeln (Editor/Desktop-Panels). + if (patch.kind) projectFocus.setKind(editing.id, patch.kind); + setEditing(null); load(); + }) .catch(e => Alert.alert('Fehler', String(e?.message || e))); - }, [editing, editName, editDesc, load]); + }, [editing, editName, editDesc, editKind, load]); const endProject = useCallback((p: Project) => { Alert.alert(`"${p.name}" beenden?`, @@ -375,6 +384,21 @@ export const ProjectsBrowser: React.FC = ({ visible = true, onClose, onAc style={[s.input, { height: 70 }]} multiline /> + setEditKind(k => (k === 'code' ? 'chat' : 'code'))} + style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 8 }} + > + ๐Ÿ’ป Code-Projekt{'\n'} + zeigt Editor + Desktop im Cockpit + + + + + setEditing(null)} style={[s.modalBtn, { backgroundColor: '#2A2A3E' }]}> Abbrechen diff --git a/android/src/services/brainApi.ts b/android/src/services/brainApi.ts index e26aaab..3daba4b 100644 --- a/android/src/services/brainApi.ts +++ b/android/src/services/brainApi.ts @@ -600,14 +600,32 @@ export const brainApi = { }); }, - /** Projekt-Metadaten patchen (name / description / hidden). */ - updateProject(projectId: string, patch: Partial>): Promise { + /** Projekt-Metadaten patchen (name / description / hidden / kind). */ + updateProject(projectId: string, patch: Partial>): Promise { return _send(`/projects/${encodeURIComponent(projectId)}`, { method: 'PATCH', body: patch, }); }, + /** Projekt manuell als Code-Projekt / normalen Chat markieren. */ + setProjectKind(projectId: string, kind: 'code' | 'chat'): Promise { + return _send(`/projects/${encodeURIComponent(projectId)}`, { + method: 'PATCH', + body: { kind }, + }); + }, + + /** Vorhandene Code-Dateien eines Projekts auflisten (/shared/projects//). */ + listProjectFiles(projectId: string): Promise<{ projectId: string; files: { path: string; size: number }[] }> { + return _send(`/projects/${encodeURIComponent(projectId)}/files`); + }, + + /** Inhalt einer Projekt-Datei laden. */ + readProjectFile(projectId: string, path: string): Promise<{ projectId: string; path: string; content: string }> { + return _send(`/projects/${encodeURIComponent(projectId)}/file?path=${encodeURIComponent(path)}`); + }, + /** Projekt verstecken / wieder sichtbar machen (bleibt voll nutzbar). */ setProjectHidden(projectId: string, hidden: boolean): Promise { return _send(`/projects/${encodeURIComponent(projectId)}`, { diff --git a/android/src/workspace/tiles/CodeEditorTile.tsx b/android/src/workspace/tiles/CodeEditorTile.tsx index 65fa364..34518b1 100644 --- a/android/src/workspace/tiles/CodeEditorTile.tsx +++ b/android/src/workspace/tiles/CodeEditorTile.tsx @@ -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//: + * - 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 = { + 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 = ({ projectId }) => { const webRef = useRef(null); - const [files, setFiles] = useState(() => codeFile.getFiles(projectId)); - const [currentPath, setCurrentPath] = useState(files[0]?.path ?? null); + // Pfade aus dem Brain (vorhandene Dateien) โ€” mit Live-Dateien gemergt. + const [serverPaths, setServerPaths] = useState([]); + const [currentPath, setCurrentPath] = useState(null); + const [loadErr, setLoadErr] = useState(''); const readyRef = useRef(false); const currentPathRef = useRef(currentPath); currentPathRef.current = currentPath; + // Vereinigte, sortierte Dateiliste (Live-Spiegel + Server-Dateien). + const files = useMemo(() => { + const set = new Set(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) => { 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 = ({ 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 = ({ projectId }) => { {files.length === 0 ? ( - Noch keine Datei + {loadErr ? `Fehler: ${loadErr}` : 'Noch keine Datei in diesem Projekt'} ) : ( - {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 ( - setCurrentPath(f.path)} style={[styles.tab, active && styles.tabActive]}> + setCurrentPath(path)} style={[styles.tab, active && styles.tabActive]}> {name} ); diff --git a/aria-brain/main.py b/aria-brain/main.py index a195376..b36695f 100644 --- a/aria-brain/main.py +++ b/aria-brain/main.py @@ -827,17 +827,73 @@ class ProjectUpdateBody(BaseModel): name: Optional[str] = None description: Optional[str] = None hidden: Optional[bool] = None + kind: Optional[str] = None # 'code' | 'chat' โ€” manuell setzbar (App/Diagnostic) @app.patch("/projects/{project_id}") def projects_update(project_id: str, body: ProjectUpdateBody): patch = body.dict(exclude_unset=True) + if "kind" in patch and patch["kind"] not in ("code", "chat", None): + raise HTTPException(status_code=400, detail="kind muss 'code' oder 'chat' sein") p = projects_mod.update_project(project_id, patch) if p is None: raise HTTPException(status_code=404, detail=f"Projekt {project_id} nicht gefunden") return p +# โ”€โ”€ Code-Dateien eines Projekts (/shared/projects//) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Der Live-Editor streamt ARIAs Writes; diese Endpoints liefern zusaetzlich die +# BEREITS vorhandenen Dateien, damit der Editor beim Oeffnen nicht leer ist. +_PROJECT_FILES_ROOT = "/shared/projects" +_PROJECT_FILE_MAX = 512 * 1024 + + +def _project_dir(project_id: str) -> str: + base = os.path.realpath(os.path.join(_PROJECT_FILES_ROOT, project_id or "")) + root = os.path.realpath(_PROJECT_FILES_ROOT) + if base != root and not base.startswith(root + os.sep): + raise HTTPException(status_code=400, detail="ungueltige project_id") + return base + + +@app.get("/projects/{project_id}/files") +def project_files(project_id: str): + base = _project_dir(project_id) + out = [] + if os.path.isdir(base): + for dirpath, dirs, files in os.walk(base): + dirs[:] = [d for d in dirs if d not in + (".git", "node_modules", "__pycache__", ".venv", "venv")] + for f in files: + full = os.path.join(dirpath, f) + rel = os.path.relpath(full, base).replace("\\", "/") + try: + sz = os.path.getsize(full) + except OSError: + sz = 0 + out.append({"path": rel, "size": sz}) + out.sort(key=lambda x: x["path"]) + return {"projectId": project_id, "files": out} + + +@app.get("/projects/{project_id}/file") +def project_file(project_id: str, path: str): + base = _project_dir(project_id) + target = os.path.realpath(os.path.join(base, path)) + if target != base and not target.startswith(base + os.sep): + raise HTTPException(status_code=400, detail="Pfad ausserhalb des Projekts") + if not os.path.isfile(target): + raise HTTPException(status_code=404, detail="Datei nicht gefunden") + if os.path.getsize(target) > _PROJECT_FILE_MAX: + raise HTTPException(status_code=413, detail="Datei zu gross fuer den Editor") + try: + with open(target, "r", encoding="utf-8", errors="replace") as f: + content = f.read() + except Exception as exc: + raise HTTPException(status_code=500, detail=str(exc)) + return {"projectId": project_id, "path": path, "content": content} + + @app.get("/conversation/stats") def conversation_stats(): return conversation().stats() diff --git a/diagnostic/index.html b/diagnostic/index.html index 56cec18..6bf89d1 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -3194,12 +3194,14 @@ ${hidden ? '๐Ÿ™ˆ' : '๐Ÿ“'} ${escapeHtml(p.name)} ${hidden ? 'versteckt' : ''} ${ended ? 'beendet' : ''} + ${p.kind === 'code' ? '</> Code' : ''} ${isActive ? 'โœ“ AKTIV' : ''} ${p.description ? `
${escapeHtml(p.description)}
` : ''}
${p.turn_count} Turns ยท zuletzt ${since}
+ ${!ended ? `` : ''} @@ -3246,6 +3248,18 @@ } catch (e) { alert('Verstecken/Anzeigen fehlgeschlagen: ' + e.message); } } + async function setProjectKind(id, kind) { + try { + const r = await fetch(`/api/brain/projects/${encodeURIComponent(id)}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ kind }), + }); + if (!r.ok) throw new Error('HTTP ' + r.status); + loadProjects(); + } catch (e) { alert('Code-Markierung fehlgeschlagen: ' + e.message); } + } + async function switchProject(projectId) { try { await fetch('/api/brain/projects/switch', {