feat(app): Workspace-Umbau Schritt 8 — Live-Reveal + Layout-Persistenz

- Progressive Reveal: ChatScreen spiegelt bei project_changed den Projekt-Typ
  sofort in projectFocus (kind_changed nach set_project_kind) → Editor/Desktop-
  Kacheln erscheinen live, nicht erst beim Reconnect.
- useWorkspaceLayout: merkt pro Projekt die zuletzt fokussierte Kachel
  (AsyncStorage aria_workspace_layout) und stellt sie beim Zurueckkehren in ein
  Code-Projekt wieder her.

tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 00:04:03 +02:00
co-authored by Claude Opus 4.8
parent 0b35ea9bde
commit ea3de70d05
3 changed files with 63 additions and 0 deletions
+5
View File
@@ -987,6 +987,11 @@ const ChatScreen: React.FC = () => {
if (p.id && p.name) { if (p.id && p.name) {
setProjectNameById(prev => ({ ...prev, [p.id]: p.name })); setProjectNameById(prev => ({ ...prev, [p.id]: p.name }));
} }
// Projekt-Typ sofort spiegeln → Editor/Desktop-Kacheln erscheinen live
// (z.B. nach set_project_kind), nicht erst beim naechsten Reconnect.
if (p.id && (p.kind === 'code' || p.kind === 'chat')) {
projectFocus.setKind(p.id, p.kind);
}
if (action === 'entered' || action === 'created') { if (action === 'entered' || action === 'created') {
if (p.id) setFocusedProjectId(p.id); if (p.id) setFocusedProjectId(p.id);
} else if (action === 'exited') { } else if (action === 'exited') {
+18
View File
@@ -25,6 +25,7 @@ import {
boundsOf, Camera, focusCamera, overviewCamera, TileId, TILE_RECTS, toLocal, boundsOf, Camera, focusCamera, overviewCamera, TileId, TILE_RECTS, toLocal,
} from './layout'; } from './layout';
import Tile from './Tile'; import Tile from './Tile';
import { useWorkspaceLayout } from './useWorkspaceLayout';
import ChatTile from './tiles/ChatTile'; import ChatTile from './tiles/ChatTile';
import CodeEditorTile from './tiles/CodeEditorTile'; import CodeEditorTile from './tiles/CodeEditorTile';
import VncTile from './tiles/VncTile'; import VncTile from './tiles/VncTile';
@@ -49,6 +50,7 @@ const WorkspaceCanvas: React.FC<Props> = ({ projectId, visibleTiles, subtitles }
const worldW = bounds.w; const worldW = bounds.w;
const worldH = bounds.h; const worldH = bounds.h;
const single = visibleTiles.length <= 1; const single = visibleTiles.length <= 1;
const { loaded: layoutLoaded, getFocus, saveFocus } = useWorkspaceLayout(projectId);
// Kamera (shared values fuer 60fps auf dem UI-Thread). // Kamera (shared values fuer 60fps auf dem UI-Thread).
const scale = useSharedValue(1); const scale = useSharedValue(1);
@@ -69,6 +71,22 @@ const WorkspaceCanvas: React.FC<Props> = ({ projectId, visibleTiles, subtitles }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [visibleKey]); }, [visibleKey]);
// Zuletzt fokussierte Kachel pro Projekt wiederherstellen (nur Mehr-Kachel-
// Projekte; laeuft NACH dem single-Effekt oben, gewinnt also fuer Code-Projekte).
useEffect(() => {
if (!layoutLoaded || single) return;
const stored = getFocus();
if (stored === undefined) return;
if (stored === null || visibleTiles.includes(stored)) setFocusedTileId(stored);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId, layoutLoaded, visibleKey]);
// Fokus-Wahl persistieren.
useEffect(() => {
if (layoutLoaded) saveFocus(focusedTileId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [focusedTileId, layoutLoaded]);
// Kamera auf das Ziel fahren (Fokus-Rect oder Uebersicht). // Kamera auf das Ziel fahren (Fokus-Rect oder Uebersicht).
useEffect(() => { useEffect(() => {
const cam: Camera = focusedTileId const cam: Camera = focusedTileId
@@ -0,0 +1,40 @@
/**
* useWorkspaceLayout — merkt sich pro Projekt die zuletzt fokussierte Kachel,
* damit man beim Zurueckkehren in ein Code-Projekt wieder dort landet (Editor/
* Desktop) statt immer im Chat. Persistiert nach AsyncStorage (Muster wie
* aria_project_drafts).
*/
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useCallback, useEffect, useRef, useState } from 'react';
import { TileId } from './layout';
const KEY = 'aria_workspace_layout';
interface Entry { focus: TileId | null }
type LayoutMap = Record<string, Entry>;
const keyOf = (projectId: string) => projectId || '__main__';
export function useWorkspaceLayout(projectId: string) {
const mapRef = useRef<LayoutMap>({});
const [loaded, setLoaded] = useState(false);
useEffect(() => {
AsyncStorage.getItem(KEY).then((v) => {
if (v) { try { mapRef.current = JSON.parse(v) || {}; } catch { /* ignore */ } }
setLoaded(true);
}).catch(() => setLoaded(true));
}, []);
const getFocus = useCallback((): TileId | null | undefined => {
return mapRef.current[keyOf(projectId)]?.focus;
}, [projectId]);
const saveFocus = useCallback((focus: TileId | null) => {
mapRef.current = { ...mapRef.current, [keyOf(projectId)]: { focus } };
AsyncStorage.setItem(KEY, JSON.stringify(mapRef.current)).catch(() => {});
}, [projectId]);
return { loaded, getFocus, saveFocus };
}