// Workspace — the active project selection shared across pages. // // One switcher in the AppBar drives two things at once (see // docs/architecture/projects.md in the platform repo, § Studio): // the page filter (audit, approvals) AND the label new runs are // stamped with. "All projects" stays reachable — the selection is // a filter, not a jail. Sealed areas are NOT listed here; they are // their own hub instance and arrive with the stage-3 connection // switch. import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'hub.dart'; class Workspace extends ChangeNotifier { Workspace._(); static final Workspace instance = Workspace._(); static const _prefsKey = 'workspace.active'; /// Registry projects (`general` first, as ordered by the hub). List _projects = const []; List get projects => _projects; /// Active project slug; empty = all projects (no filter). String _activeSlug = ''; String get activeSlug => _activeSlug; bool get isAll => _activeSlug.isEmpty; /// The active project's registry entry, or null for "all". ProjectRef? get active { if (_activeSlug.isEmpty) return null; for (final p in _projects) { if (p.slug == _activeSlug) return p; } return null; } bool _loaded = false; /// Restore the persisted selection and pull the registry once. /// Safe to call repeatedly; only the first call restores. Future ensureLoaded() async { if (!_loaded) { _loaded = true; try { final prefs = await SharedPreferences.getInstance(); _activeSlug = prefs.getString(_prefsKey) ?? ''; } catch (_) { // No persisted selection — start on "all projects". } } await refresh(); } /// Re-pull the registry from the hub. Soft-fails (keeps the /// last list) so a hub restart doesn't blank the switcher; an /// active slug that vanished from the registry stays selected — /// its historical rows remain filterable, which is the honest /// behaviour for an audit surface. Future refresh() async { try { final fresh = await HubService.instance.listProjects(); if (!listEquals(fresh, _projects)) { _projects = fresh; notifyListeners(); } } catch (_) { // Hub unreachable — the pages already surface that state. } } /// Test hook: seed registry + selection without a hub and mark /// the persisted state as restored so [ensureLoaded] won't /// overwrite the seed. @visibleForTesting void debugSeed({ required List projects, required String active, }) { _projects = projects; _activeSlug = active; _loaded = true; notifyListeners(); } /// Switch the active workspace ('' = all projects) and persist. Future setActive(String slug) async { if (slug == _activeSlug) return; _activeSlug = slug; notifyListeners(); try { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_prefsKey, slug); } catch (_) { // Persistence is best-effort; the in-memory switch stands. } } }