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 <noreply@anthropic.com>
106 lines
2.8 KiB
TypeScript
106 lines
2.8 KiB
TypeScript
/**
|
|
* 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<string, string>;
|
|
projectKindById: Record<string, ProjectKind>;
|
|
}
|
|
|
|
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<string, string>): 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<string, ProjectKind>): 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;
|