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:
2026-06-13 21:55:02 +02:00
co-authored by Claude Opus 4.7
parent 1baa1a7a08
commit 1fb512c2fd
7 changed files with 359 additions and 22 deletions
+46 -1
View File
@@ -93,6 +93,7 @@ import SkillBrowser from '../components/SkillBrowser';
import OAuthBrowser from '../components/OAuthBrowser';
import VoiceIdEnrollment from '../components/VoiceIdEnrollment';
import ProjectsBrowser from '../components/ProjectsBrowser';
import brainApi from '../services/brainApi';
import { isVerboseLogging, setVerboseLogging, isDebugLogsToBridge, setDebugLogsToBridge, APP_LOG_EVENT } from '../services/logger';
import {
isWakeReadySoundEnabled,
@@ -204,7 +205,9 @@ const SettingsScreen: React.FC = () => {
const [availableVoices, setAvailableVoices] = useState<Array<{name: string, size: number}>>([]);
// Datei-Manager
const [fileManagerOpen, setFileManagerOpen] = useState(false);
const [fileManagerFiles, setFileManagerFiles] = useState<Array<{name: string; path: string; size: number; mtime: number; fromAria: boolean}>>([]);
const [fileManagerFiles, setFileManagerFiles] = useState<Array<{name: string; path: string; size: number; mtime: number; fromAria: boolean; projectId?: string}>>([]);
const [fileFilterProjectId, setFileFilterProjectId] = useState<string>('__all__');
const [fileFilterProjects, setFileFilterProjects] = useState<Array<{id: string; name: string}>>([]);
const [fileManagerLoading, setFileManagerLoading] = useState(false);
const [fileManagerError, setFileManagerError] = useState('');
const [fileManagerSearch, setFileManagerSearch] = useState('');
@@ -726,6 +729,20 @@ const SettingsScreen: React.FC = () => {
return () => unsub();
}, [fileManagerOpen]);
// Beim Oeffnen des Datei-Managers: Projekt-Liste laden fuer den Filter.
useEffect(() => {
if (!fileManagerOpen) return;
brainApi.listProjects(true)
.then(list => setFileFilterProjects(list.map(p => ({ id: p.id, name: p.name }))))
.catch(() => {});
// Default-Filter: aktives Projekt (falls vorhanden), sonst "alle".
brainApi.getProjectStatus()
.then(s => {
if (s.active_id) setFileFilterProjectId(s.active_id);
})
.catch(() => {});
}, [fileManagerOpen]);
// --- QR-Code scannen ---
const openQRScanner = useCallback(() => {
@@ -962,6 +979,29 @@ const SettingsScreen: React.FC = () => {
</TouchableOpacity>
))}
</View>
{/* Projekt-Filter: scrollbare Pill-Reihe. „Alle Projekte" + „Hauptchat" +
ein Pill pro Projekt. Default = aktives Projekt (siehe useEffect oben). */}
<ScrollView horizontal showsHorizontalScrollIndicator={false}
style={{marginTop:6}} contentContainerStyle={{gap:6, paddingRight:8}}>
{[
{ id: '__all__', name: '📁 Alle Projekte' },
{ id: '', name: '💬 Hauptchat' },
...fileFilterProjects,
].map(p => (
<TouchableOpacity
key={p.id || 'mainchat'}
onPress={() => setFileFilterProjectId(p.id)}
style={{
paddingVertical:6, paddingHorizontal:12, borderRadius:14,
backgroundColor: fileFilterProjectId === p.id ? '#34C759' : '#1E1E2E',
}}
>
<Text style={{color: fileFilterProjectId === p.id ? '#fff' : '#8888AA', fontSize:12}}>
{p.name}
</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
{fileManagerLoading ? (
<Text style={{color:'#8888AA', textAlign:'center', marginTop:20}}>Lade...</Text>
@@ -972,6 +1012,11 @@ const SettingsScreen: React.FC = () => {
let files = fileManagerFiles;
if (fileManagerFilter === 'aria') files = files.filter(f => f.fromAria);
else if (fileManagerFilter === 'user') files = files.filter(f => !f.fromAria);
// Projekt-Filter: '__all__' = alles, '' = Hauptchat (kein project_id),
// sonst exakte project_id-Match.
if (fileFilterProjectId !== '__all__') {
files = files.filter(f => (f.projectId || '') === fileFilterProjectId);
}
if (fileManagerSearch) {
const q = fileManagerSearch.toLowerCase();
files = files.filter(f => f.name.toLowerCase().includes(q));