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:
@@ -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' }}>
|
||||
|
||||
Reference in New Issue
Block a user