feat(app): Multi-Threading UI — Focus-One-View + Drawer + Queue-Status-Dots

Phase 2 vom Multi-Threading-Redesign. Chat zeigt jetzt genau EINEN Kontext
(Hauptchat oder Projekt X) — die anderen laufen im Brain weiter, sichtbar
nur ueber Status-Dots im Drawer.

ChatScreen:
- Reorder-Trick + collapsible Project-Bloecke raus. messagesForRender filtert
  jetzt direkt auf focusedProjectId.
- Neuer Focus-Header oben: ☰ Drawer-Toggle + Kontext-Name + Status-Dot
  (gruen idle / gelb queue / rot arbeitet). Drawer-Icon kriegt ein Badge
  mit der Anzahl OTHERE aktiver Kontexte.
- Focus in AsyncStorage gespiegelt — Neustart restauriert den letzten Blick.
- brainApi.getProjectQueueStatus() alle 2s gepollt fuer Status-Dots.
- project_changed-Event steuert Focus-Wechsel (App-lokal, kein Brain-Roundtrip).

brainApi:
- Neuer Typ QueueContextStatus + ProjectQueueStatus.
- Methode getProjectQueueStatus() → /projects/queue-status.

ProjectsBrowser:
- Nimmt queueStatus als Prop, rendert Status-Dot pro Zeile (Hauptchat +
  Projekte).
- switchTo ruft NICHT mehr brainApi.switchProject (kein globaler active
  mehr) — direkt onActiveChanged mit dem Projekt-Objekt aus der Liste,
  schliesst danach die Modal.
- Label ✓ FOCUS statt ✓ AKTIV — praeziser fuer's neue Modell.

SettingsScreen:
- File-Manager-Filter-Default nutzt AsyncStorage statt Brain-Query.

Bewusst nicht drin (Follow-up):
- OS-Push wenn Projekt fertig ist — braucht Firebase-Setup, kommt separat
  wenn die visuellen Dots nicht reichen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 20:52:14 +02:00
parent 7927ad05ae
commit 06316da36f
4 changed files with 191 additions and 225 deletions
+50 -23
View File
@@ -33,9 +33,12 @@ interface Props {
/** Optional — wenn als Modal genutzt, sonst inline */
visible?: boolean;
onClose?: () => void;
/** Wird gerufen wenn sich das aktive Projekt aendert — ChatScreen
* refresht dann seinen Banner-State. */
/** Wird gerufen wenn Stefan ein anderes Projekt fokussiert (App-lokale
* UI-Entscheidung, wechselt den Chat-Focus). */
onActiveChanged?: (project: Project | null) => void;
/** Queue-Status pro Kontext (key "__main__" = Hauptchat, sonst project_id).
* Wenn geliefert: Status-Dot pro Zeile gerendert. */
queueStatus?: Record<string, { busy: boolean; queue_size: number }>;
}
function _fmtRel(unixSec: number): string {
@@ -48,7 +51,14 @@ function _fmtRel(unixSec: number): string {
return new Date(unixSec * 1000).toLocaleDateString('de-DE');
}
export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onActiveChanged }) => {
export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onActiveChanged, queueStatus }) => {
const _statusDot = (pid: string) => {
const s = queueStatus?.[pid];
if (!s) return { color: '#555570', label: '' };
if (s.busy) return { color: '#FF6E6E', label: 'arbeitet' };
if (s.queue_size > 0) return { color: '#FFD60A', label: `Queue: ${s.queue_size}` };
return { color: '#34C759', label: 'idle' };
};
const [projects, setProjects] = useState<Project[]>([]);
const [activeId, setActiveId] = useState<string>('');
const [loading, setLoading] = useState(false);
@@ -88,13 +98,14 @@ export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onAc
}, [visible, load]);
const switchTo = useCallback((id: string) => {
brainApi.switchProject(id)
.then(status => {
setActiveId(status.active_id || '');
onActiveChangedRef.current?.(status.active);
})
.catch(e => Alert.alert('Fehler', String(e?.message || e)));
}, []);
// Multi-Threading: Focus-Wechsel ist reine App-lokale UI-Entscheidung.
// Brain wird nicht mehr benachrichtigt (kein globaler active_project mehr).
// Wir suchen das Projekt lokal aus der Liste, damit die App den Namen kennt.
setActiveId(id);
const p = id ? (projects.find(x => x.id === id) || null) : null;
onActiveChangedRef.current?.(p);
if (onClose) onClose();
}, [projects, onClose]);
const createProject = useCallback(() => {
const name = newName.trim();
@@ -152,6 +163,7 @@ export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onAc
const renderItem = ({ item }: { item: Project }) => {
const isActive = item.id === activeId;
const dot = _statusDot(item.id);
return (
<TouchableOpacity
onPress={() => switchTo(item.id)}
@@ -160,15 +172,19 @@ export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onAc
>
<View style={{ flex: 1 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
{queueStatus && (
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: dot.color }} />
)}
<Text style={[s.rowName, isActive && { color: '#34C759' }]}>{item.name}</Text>
{item.status === 'ended' && <Text style={s.statusBadge}>beendet</Text>}
{isActive && <Text style={s.activeBadge}> AKTIV</Text>}
{isActive && <Text style={s.activeBadge}> FOCUS</Text>}
</View>
{item.description ? (
<Text style={s.rowDesc} numberOfLines={2}>{item.description}</Text>
) : null}
<Text style={s.rowMeta}>
{item.turn_count} Turns · zuletzt {_fmtRel(item.last_activity_at)}
{dot.label ? ` · ${dot.label}` : ''}
</Text>
</View>
</TouchableOpacity>
@@ -191,18 +207,29 @@ export const ProjectsBrowser: React.FC<Props> = ({ visible = true, onClose, onAc
</View>
{/* Hauptchat-Eintrag (immer oben) */}
<TouchableOpacity
onPress={() => switchTo('')}
style={[s.row, !activeId && s.rowActive]}
>
<View style={{ flex: 1 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<Text style={[s.rowName, !activeId && { color: '#34C759' }]}>💬 Hauptchat</Text>
{!activeId && <Text style={s.activeBadge}> AKTIV</Text>}
</View>
<Text style={s.rowMeta}>Standard-Verlauf, keine Projekt-Zuordnung</Text>
</View>
</TouchableOpacity>
{(() => {
const dot = _statusDot('__main__');
return (
<TouchableOpacity
onPress={() => switchTo('')}
style={[s.row, !activeId && s.rowActive]}
>
<View style={{ flex: 1 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
{queueStatus && (
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: dot.color }} />
)}
<Text style={[s.rowName, !activeId && { color: '#34C759' }]}>💬 Hauptchat</Text>
{!activeId && <Text style={s.activeBadge}> FOCUS</Text>}
</View>
<Text style={s.rowMeta}>
Standard-Verlauf, keine Projekt-Zuordnung
{dot.label ? ` · ${dot.label}` : ''}
</Text>
</View>
</TouchableOpacity>
);
})()}
{loading ? (
<View style={{ padding: 24, alignItems: 'center' }}>
+119 -197
View File
@@ -286,9 +286,15 @@ const ChatScreen: React.FC = () => {
const [searchQuery, setSearchQuery] = useState('');
const [searchVisible, setSearchVisible] = useState(false);
const [projectsVisible, setProjectsVisible] = useState(false);
const [activeProject, setActiveProject] = useState<BrainProject | null>(null);
// Lookup-Map id → Projekt — fuer Header-Names im Chat-Verlauf
// Focus-One-View: welchen Chat sieht Stefan gerade?
// Leer = Hauptchat, sonst die project_id. Multi-Threading:
// Wechsel des Focus stoppt NICHT ARIAs Arbeit in anderen Projekten —
// die laufen im Brain weiter, wir sehen sie hier nur nicht.
const [focusedProjectId, setFocusedProjectId] = useState<string>('');
// Lookup-Map id → Projekt (fuer Drawer + Referenzen)
const [projectNameById, setProjectNameById] = useState<Record<string, string>>({});
// Queue-Status pro Kontext — polled alle 2s, fuer Status-Dots im Drawer
const [queueStatus, setQueueStatus] = useState<Record<string, { busy: boolean; queue_size: number }>>({});
const [searchIndex, setSearchIndex] = useState(0); // welcher Treffer aktiv ist
const [pendingAttachments, setPendingAttachments] = useState<{file: any, isPhoto: boolean}[]>([]);
const [agentActivity, setAgentActivity] = useState<{activity: string, tool: string}>({activity: 'idle', tool: ''});
@@ -465,27 +471,53 @@ const ChatScreen: React.FC = () => {
}, [dispatchWithAck]);
// TTS- + GPS-Settings beim Mount + alle 2s neu laden (damit Settings-Toggle
// sofort greift, ohne Context- oder Event-System)
// Aktives Projekt initial laden + bei RVS-Reconnect refreshen.
// Wird zusaetzlich nach jedem chat-Response refreshed (siehe handleAriaMessage).
// Projekt-Namen laden (Lookup-Map) + focusedProjectId aus AsyncStorage
// wiederherstellen (Default = Hauptchat wenn nichts gespeichert). Der
// Brain hat mit Multi-Threading keinen global-aktiven Projekt-State mehr;
// Focus ist reine App-lokale UI-Info.
useEffect(() => {
const loadProject = () => {
brainApi.getProjectStatus()
.then(s => {
setActiveProject(s.active || null);
// Lookup-Map fuellen damit der Chat-Verlauf Header mit Namen rendern kann
const loadNames = () => {
brainApi.listProjects(true)
.then(list => {
const map: Record<string, string> = {};
for (const p of (s.projects || [])) map[p.id] = p.name;
if (s.active) map[s.active.id] = s.active.name;
for (const p of list) map[p.id] = p.name;
setProjectNameById(prev => ({ ...prev, ...map }));
})
.catch(() => {});
};
loadProject();
const unsub = rvs.onStateChange(state => { if (state === 'connected') loadProject(); });
loadNames();
// Letzten Focus aus Storage restoren
AsyncStorage.getItem('aria_focused_project_id').then(v => {
if (v && typeof v === 'string') setFocusedProjectId(v);
}).catch(() => {});
const unsub = rvs.onStateChange(state => { if (state === 'connected') loadNames(); });
return () => unsub();
}, []);
// Focus in Storage spiegeln damit der letzte Kontext nach Neustart wieder
// da ist. Kein zwingender UX-Fix (Default = Hauptchat waere auch ok), aber
// fuer den Auto-Fall angenehm.
useEffect(() => {
AsyncStorage.setItem('aria_focused_project_id', focusedProjectId).catch(() => {});
}, [focusedProjectId]);
// Queue-Status alle 2s pollen — fuers Status-Dot im Focus-Header und
// fuer die Drawer-Anzeige. Nur wenn RVS verbunden ist (sonst 30s Timeout).
useEffect(() => {
let cancelled = false;
const poll = async () => {
if (rvs.getState() !== 'connected') return;
try {
const s = await brainApi.getProjectQueueStatus();
if (cancelled) return;
setQueueStatus(s.contexts || {});
} catch {}
};
poll();
const iv = setInterval(poll, 2000);
return () => { cancelled = true; clearInterval(iv); };
}, []);
useEffect(() => {
const loadSettings = async () => {
const enabled = await AsyncStorage.getItem('aria_tts_enabled');
@@ -825,12 +857,20 @@ const ChatScreen: React.FC = () => {
return;
}
// project_changed: ARIA hat in einem Tool-Call ein Projekt erstellt /
// betreten / verlassen / beendet. Banner refreshen.
// project_changed: ARIA hat via Tool ein Projekt erstellt/betreten/exited/beendet.
// App entscheidet ob sie den Focus wechselt basierend auf action + payload.
if (message.type === 'project_changed') {
brainApi.getProjectStatus()
.then(s => setActiveProject(s.active || null))
.catch(() => {});
const p: any = message.payload || {};
const action = p.action || '';
// Neuer Projekt-Name in Lookup-Map merken
if (p.id && p.name) {
setProjectNameById(prev => ({ ...prev, [p.id]: p.name }));
}
if (action === 'entered' || action === 'created') {
if (p.id) setFocusedProjectId(p.id);
} else if (action === 'exited') {
setFocusedProjectId('');
}
return;
}
@@ -1629,116 +1669,14 @@ const ChatScreen: React.FC = () => {
[messages],
);
// Projekt-Bloecke: alle Nachrichten eines Projekts erscheinen als EIN Block
// — auch wenn der User zwischenzeitlich raus + wieder rein gegangen ist.
// Dafuer ordnen wir die Nachrichten so um, dass alle Messages eines Projekts
// contiguous werden, verankert am LETZTEN Aktivitaets-Timestamp des Projekts.
// Hauptchat-Nachrichten bleiben chronologisch dazwischen.
const [collapsedProjects, setCollapsedProjects] = useState<Set<string>>(new Set());
const reorderedMessages = useMemo(() => {
// Hauptchat-Nachrichten + Projekt-Gruppen trennen
const hauptchat: ChatMessage[] = [];
const groups = new Map<string, ChatMessage[]>();
for (const m of chatVisibleMessages) {
const pid = m.projectId || '';
if (!pid) hauptchat.push(m);
else {
const g = groups.get(pid);
if (g) g.push(m); else groups.set(pid, [m]);
}
}
// Events: jede Hauptchat-Bubble + jede Projekt-Gruppe als 1 Event
type Event =
| { ts: number; kind: 'msg'; m: ChatMessage }
| { ts: number; kind: 'group'; msgs: ChatMessage[] };
const events: Event[] = [];
for (const m of hauptchat) events.push({ ts: m.timestamp, kind: 'msg', m });
for (const [, msgs] of groups) {
const sorted = [...msgs].sort((a, b) => a.timestamp - b.timestamp);
const anchorTs = sorted[sorted.length - 1].timestamp;
events.push({ ts: anchorTs, kind: 'group', msgs: sorted });
}
events.sort((a, b) => a.ts - b.ts);
const out: ChatMessage[] = [];
for (const e of events) {
if (e.kind === 'msg') out.push(e.m);
else out.push(...e.msgs);
}
return out;
}, [chatVisibleMessages]);
const projectMeta = useMemo(() => {
// Pre-compute: welche Message ist Erst-Element ihrer Projekt-Gruppe?
// Plus: wieviele Messages pro Projekt insgesamt (fuer Header-Count).
const firstOfGroup = new Set<string>();
const counts = new Map<string, number>();
let lastPid: string | null = null;
for (const m of reorderedMessages) {
const pid = m.projectId || '';
if (pid) counts.set(pid, (counts.get(pid) || 0) + 1);
if (pid && pid !== lastPid) firstOfGroup.add(m.id);
lastPid = pid || null;
}
return { firstOfGroup, counts };
}, [reorderedMessages]);
// Render-Filter: bei collapsed Projekten zeigen wir NUR das erste Message-
// Item der Gruppe (das traegt den Header). Restliche Messages werden ausge-
// blendet — Header allein steht dann zwischen Hauptchat-Bubbles.
// Focus-One-View (Multi-Threading, 06/2026): Chat zeigt NUR die Nachrichten
// des gerade fokussierten Kontexts. Hauptchat (focusedProjectId leer) →
// alle ungeтагtgeд Nachrichten. Projekt X aktiv → nur Nachrichten mit
// projectId === X. ARIA arbeitet weiterhin in allen Kontexten parallel;
// wir sehen nur den einen.
const messagesForRender = useMemo(() => {
return reorderedMessages.filter(m => {
const pid = m.projectId || '';
if (!pid) return true;
if (!collapsedProjects.has(pid)) return true;
return projectMeta.firstOfGroup.has(m.id);
});
}, [reorderedMessages, collapsedProjects, projectMeta]);
// Auto-Collapse beim Projekt-Wechsel: altes Projekt einklappen, neues aufklappen.
const prevActiveIdRef = useRef<string>('');
useEffect(() => {
const newActive = activeProject?.id || '';
const prevActive = prevActiveIdRef.current;
if (newActive === prevActive) return;
setCollapsedProjects(prev => {
const next = new Set(prev);
if (prevActive) next.add(prevActive);
if (newActive) next.delete(newActive);
return next;
});
prevActiveIdRef.current = newActive;
}, [activeProject]);
// Default: alle Projekte einklappen außer dem aktiven (Stefan-Vision:
// „beim ersten Öffnen sind alle projekte eingeklappt").
const collapseInitialized = useRef(false);
useEffect(() => {
if (collapseInitialized.current) return;
if (chatVisibleMessages.length === 0) return;
const allPids = new Set<string>();
for (const m of chatVisibleMessages) {
if (m.projectId) allPids.add(m.projectId);
}
if (allPids.size === 0) return;
const activePid = activeProject?.id || '';
setCollapsedProjects(prev => {
const next = new Set(prev);
for (const pid of allPids) {
if (pid !== activePid) next.add(pid);
}
return next;
});
collapseInitialized.current = true;
}, [chatVisibleMessages, activeProject]);
const toggleProjectCollapse = useCallback((projectId: string) => {
setCollapsedProjects(prev => {
const next = new Set(prev);
if (next.has(projectId)) next.delete(projectId); else next.add(projectId);
return next;
});
}, []);
return chatVisibleMessages.filter(m => (m.projectId || '') === focusedProjectId);
}, [chatVisibleMessages, focusedProjectId]);
const invertedMessages = useMemo(() => [...messagesForRender].reverse(), [messagesForRender]);
@@ -1942,7 +1880,7 @@ const ChatScreen: React.FC = () => {
const location = await getCurrentLocation();
const cmid = nextClientMsgId();
const activePid = activeProject?.id || '';
const activePid = focusedProjectId;
const userMsg: ChatMessage = {
id: nextId(),
sender: 'user',
@@ -2461,44 +2399,6 @@ const ChatScreen: React.FC = () => {
);
};
// Wrapper: setzt einen einklappbaren Projekt-Header VOR die Bubble wenn
// diese das erste Element ihrer Projekt-Gruppe ist. Bei collapsed
// → nur Header, keine Bubble. Hauptchat-Nachrichten gehen ohne Header durch.
const renderMessageWithProjectHeader = ({ item }: { item: ChatMessage }) => {
const pid = item.projectId || '';
if (!pid) return renderMessage({ item });
const isFirstOfGroup = projectMeta.firstOfGroup.has(item.id);
if (!isFirstOfGroup) return renderMessage({ item });
const isCollapsed = collapsedProjects.has(pid);
const projectName = projectNameById[pid] || pid;
const projectCount = projectMeta.counts.get(pid) || 0;
const header = (
<TouchableOpacity
onPress={() => toggleProjectCollapse(pid)}
style={{
marginVertical: 6, marginHorizontal: 8, paddingHorizontal: 10, paddingVertical: 8,
borderRadius: 6, backgroundColor: 'rgba(52,199,89,0.10)',
borderLeftWidth: 3, borderLeftColor: '#34C759',
flexDirection: 'row', alignItems: 'center', gap: 6,
}}
>
<Text style={{ fontSize: 14, color: '#34C759', fontWeight: '700', flex: 1 }} numberOfLines={1}>
{isCollapsed ? '▶' : '▼'} 📁 {projectName}
</Text>
<Text style={{ fontSize: 11, color: '#8888AA' }}>
{projectCount} {projectCount === 1 ? 'Nachricht' : 'Nachrichten'}
</Text>
</TouchableOpacity>
);
if (isCollapsed) return header;
return (
<View>
{header}
{renderMessage({ item })}
</View>
);
};
// Extrahiert kopierbare Items aus dem Bubble-Text (URLs, Mails, Telefon).
// Wird vom Long-Press/Copy-Menu genutzt damit Stefan den einzelnen Wert
// teilen kann ohne den umliegenden Text mitzunehmen.
@@ -2651,41 +2551,63 @@ const ChatScreen: React.FC = () => {
})()}
{/* Projekt-Indicator: zeigt Hauptchat oder aktives Projekt */}
<View
style={{
flexDirection: 'row', alignItems: 'center',
paddingHorizontal: 12, paddingVertical: 6,
backgroundColor: activeProject ? 'rgba(52,199,89,0.10)' : 'transparent',
borderBottomWidth: activeProject ? 2 : 1,
borderColor: activeProject ? '#34C759' : '#1E1E2E',
}}
>
<TouchableOpacity onPress={() => setProjectsVisible(true)} style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ fontSize: 13, color: activeProject ? '#34C759' : '#8888AA', fontWeight: activeProject ? '700' : '500', flex: 1 }} numberOfLines={1}>
{activeProject ? `📁 ${activeProject.name}` : '💬 Hauptchat'}
</Text>
<Text style={{ fontSize: 11, color: '#555570', marginRight: 8 }}>
{activeProject ? 'wechseln ' : 'Projekte '}
</Text>
</TouchableOpacity>
{activeProject && (
<TouchableOpacity
onPress={() => {
brainApi.switchProject('').then(s => setActiveProject(s.active || null)).catch(() => {});
{/* Focus-Indicator + Drawer-Toggle. Multi-Threading: das ist reine
Anzeige „was sehe ich gerade" — ARIA arbeitet gleichzeitig in
allen Kontexten weiter, wir zeigen hier nur einen. */}
{(() => {
const isMain = !focusedProjectId;
const focusedName = isMain ? '' : (projectNameById[focusedProjectId] || focusedProjectId);
const focusedQueue = queueStatus[isMain ? '__main__' : focusedProjectId];
const dot = focusedQueue?.busy
? { color: '#FF6E6E', label: 'arbeitet' }
: focusedQueue?.queue_size
? { color: '#FFD60A', label: `Queue: ${focusedQueue.queue_size}` }
: { color: '#34C759', label: 'idle' };
// Anzahl anderer Kontexte die gerade aktiv sind (fuer Drawer-Badge)
const otherActive = Object.entries(queueStatus).filter(([k, v]) => {
const kFocus = isMain ? '__main__' : focusedProjectId;
if (k === kFocus) return false;
return v.busy || v.queue_size > 0;
}).length;
return (
<View
style={{
flexDirection: 'row', alignItems: 'center',
paddingHorizontal: 12, paddingVertical: 8,
backgroundColor: isMain ? '#1A1A26' : 'rgba(52,199,89,0.10)',
borderBottomWidth: 2,
borderColor: isMain ? '#1E1E2E' : '#34C759',
}}
style={{ paddingHorizontal: 8, paddingVertical: 2, borderRadius: 4, backgroundColor: 'rgba(255,255,255,0.08)' }}
hitSlop={{top:8,bottom:8,left:8,right:8}}
>
<Text style={{ fontSize: 11, color: '#E0E0F0', fontWeight: '600' }}>× Hauptchat</Text>
</TouchableOpacity>
)}
</View>
<TouchableOpacity
onPress={() => setProjectsVisible(true)}
style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}
hitSlop={{top:6,bottom:6,left:6,right:6}}
>
<Text style={{ fontSize: 20 }}></Text>
{otherActive > 0 && (
<View style={{ backgroundColor: '#FF6E6E', borderRadius: 8, minWidth: 16, height: 16, paddingHorizontal: 4, alignItems: 'center', justifyContent: 'center' }}>
<Text style={{ color: '#fff', fontSize: 10, fontWeight: '700' }}>{otherActive}</Text>
</View>
)}
</TouchableOpacity>
<View style={{ flex: 1, marginLeft: 10, flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<Text style={{ fontSize: 14, color: isMain ? '#E0E0F0' : '#34C759', fontWeight: '700', flex: 1 }} numberOfLines={1}>
{isMain ? '💬 Hauptchat' : `📁 ${focusedName}`}
</Text>
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: dot.color }} />
<Text style={{ fontSize: 10, color: '#8888AA' }}>{dot.label}</Text>
</View>
</View>
);
})()}
{/* Projekt-Modal */}
{/* Projekt-Drawer als Modal */}
<ProjectsBrowser
visible={projectsVisible}
onClose={() => setProjectsVisible(false)}
onActiveChanged={(p) => setActiveProject(p)}
onActiveChanged={(p) => setFocusedProjectId(p?.id || '')}
queueStatus={queueStatus}
/>
{/* Suchleiste mit Treffer-Navigation */}
@@ -2770,7 +2692,7 @@ const ChatScreen: React.FC = () => {
}, 300);
}}
keyExtractor={item => item.id}
renderItem={renderMessageWithProjectHeader}
renderItem={renderMessage}
contentContainerStyle={styles.messageList}
showsVerticalScrollIndicator={false}
ListEmptyComponent={
+5 -5
View File
@@ -735,11 +735,11 @@ const SettingsScreen: React.FC = () => {
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);
})
// Default-Filter: fokussiertes Projekt aus AsyncStorage (falls Stefan
// grade in einem drin ist), sonst "alle". Multi-Threading: Focus ist
// App-lokal, kein Brain-Query mehr.
AsyncStorage.getItem('aria_focused_project_id')
.then(pid => { if (pid) setFileFilterProjectId(pid); })
.catch(() => {});
}, [fileManagerOpen]);
+17
View File
@@ -169,6 +169,16 @@ export interface ProjectStatus {
projects: Project[];
}
/** Queue-Status pro Kontext — was gerade arbeitet, was wartet.
* Key "__main__" = Hauptchat, sonst project_id. */
export interface QueueContextStatus {
busy: boolean;
queue_size: number;
}
export interface ProjectQueueStatus {
contexts: Record<string, QueueContextStatus>;
}
/** Skill-Manifest wie aus Brain `/skills/list` zurueckkommt. */
export interface Skill {
name: string;
@@ -590,6 +600,13 @@ export const brainApi = {
body: patch,
});
},
/** Queue-Status: pro Kontext (project_id oder __main__ fuer Hauptchat)
* ob gerade ein Request in Verarbeitung ist + wieviele in der Queue warten.
* Wird fuer Status-Dots im Drawer periodisch gepollt. */
getProjectQueueStatus(): Promise<ProjectQueueStatus> {
return _send('/projects/queue-status');
},
};
export default brainApi;