From 80b534cbab87e72393bf5320c2d1487e62a26dce Mon Sep 17 00:00:00 2001 From: duffyduck Date: Thu, 16 Jul 2026 23:36:09 +0200 Subject: [PATCH] =?UTF-8?q?feat(app):=20Workspace-Umbau=20Schritt=202=20?= =?UTF-8?q?=E2=80=94=20projectFocus-Spiegel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neues Singleton services/projectFocus.ts (Publish/Subscribe wie rvs.ts): haelt focusedProjectId, Projekt-Namen und Projekt-Kind. ChatScreen publiziert Focus + Namen EINWEG hinein (rein additive Effekte), damit der kommende Workspace-Canvas den aktiven Kontext und den Code-Projekt-Status kennt, ohne dass ChatScreen den Workspace kennt oder umgebaut wird. Verhaltensneutral, tsc clean. Co-Authored-By: Claude Opus 4.8 --- android/src/screens/ChatScreen.tsx | 7 ++ android/src/services/projectFocus.ts | 105 +++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 android/src/services/projectFocus.ts diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index 503a4b0..e1208f9 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -38,6 +38,7 @@ import audioService from '../services/audio'; import wakeWordService, { loadPassiveListenMs } from '../services/wakeword'; import ProjectsBrowser from '../components/ProjectsBrowser'; import brainApi, { Project as BrainProject } from '../services/brainApi'; +import projectFocus from '../services/projectFocus'; import phoneCallService from '../services/phoneCall'; import { playWakeReadySound } from '../services/wakeReadySound'; import { @@ -554,6 +555,12 @@ const ChatScreen: React.FC = () => { } }, [focusedProjectId]); + // Focus + Projekt-Namen EINWEG in den projectFocus-Spiegel publizieren, + // damit der Workspace-Canvas den aktiven Kontext + Code-Projekt-Status + // kennt. Rein additiv — aendert ChatScreens eigenes Verhalten nicht. + useEffect(() => { projectFocus.setFocus(focusedProjectId); }, [focusedProjectId]); + useEffect(() => { projectFocus.setNames(projectNameById); }, [projectNameById]); + // inputText-Spiegel (damit der Draft-Wechsel oben inputText nicht als Dep braucht). useEffect(() => { inputTextRef.current = inputText; }, [inputText]); diff --git a/android/src/services/projectFocus.ts b/android/src/services/projectFocus.ts new file mode 100644 index 0000000..b01917d --- /dev/null +++ b/android/src/services/projectFocus.ts @@ -0,0 +1,105 @@ +/** + * projectFocus — leichter Publish/Subscribe-Spiegel des aktuell fokussierten + * Projekt-Kontexts. + * + * ChatScreen bleibt die Quelle der Wahrheit fuer sein eigenes Rendering und + * publiziert hier bei jedem Focus-/Namens-/Kind-Wechsel EINWEG hinein. Der + * Workspace-Canvas liest/abonniert das Singleton, um zu wissen welches Projekt + * gerade aktiv ist und ob es ein Code-Projekt ist — ohne dass ChatScreen den + * Workspace kennen oder umgebaut werden muss. + * + * Muster wie services/rvs.ts (Singleton mit Listener-Liste + Unsubscribe). + */ + +export type ProjectKind = 'code' | 'chat'; + +export interface FocusSnapshot { + /** '' = Hauptchat, sonst Projekt-ID */ + focusedProjectId: string; + projectNameById: Record; + projectKindById: Record; +} + +type Sub = (snap: FocusSnapshot) => void; + +class ProjectFocus { + private snap: FocusSnapshot = { + focusedProjectId: '', + projectNameById: {}, + projectKindById: {}, + }; + private subs: Sub[] = []; + + // --- Getter (synchron, fuer Nicht-Reaktive Leser) --- + + get(): FocusSnapshot { + return this.snap; + } + + getFocusedProjectId(): string { + return this.snap.focusedProjectId; + } + + getProjectName(id: string): string { + return this.snap.projectNameById[id] || id; + } + + /** Default 'chat' — ein Projekt ist erst 'code' wenn es explizit so + * markiert wurde (set_project_kind) oder ein Code-/Desktop-Signal kam. */ + getProjectKind(id: string): ProjectKind { + return this.snap.projectKindById[id] || 'chat'; + } + + // --- Publisher (von ChatScreen aufgerufen) --- + + setFocus(id: string): void { + if (this.snap.focusedProjectId === id) return; + this.snap = { ...this.snap, focusedProjectId: id }; + this.emit(); + } + + setNames(map: Record): void { + // Flacher Merge — behaelt bereits bekannte Namen, ueberschreibt neue. + this.snap = { + ...this.snap, + projectNameById: { ...this.snap.projectNameById, ...map }, + }; + this.emit(); + } + + setKind(id: string, kind: ProjectKind): void { + if (this.snap.projectKindById[id] === kind) return; + this.snap = { + ...this.snap, + projectKindById: { ...this.snap.projectKindById, [id]: kind }, + }; + this.emit(); + } + + setKinds(map: Record): void { + this.snap = { + ...this.snap, + projectKindById: { ...this.snap.projectKindById, ...map }, + }; + this.emit(); + } + + // --- Abo --- + + /** Registriert einen Listener und liefert sofort den aktuellen Snapshot. */ + subscribe(cb: Sub): () => void { + this.subs.push(cb); + cb(this.snap); + return () => { + this.subs = this.subs.filter(s => s !== cb); + }; + } + + private emit(): void { + const s = this.snap; + this.subs.forEach(cb => cb(s)); + } +} + +const projectFocus = new ProjectFocus(); +export default projectFocus;