From 7d4cfb9d6684c2fc10278121878f5ac2e781c649 Mon Sep 17 00:00:00 2001 From: Joachim Rosskopf Date: Sun, 31 May 2026 06:52:40 +0200 Subject: [PATCH] fix(explore): auto-focus the engagement spine that has event history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workspace auto-focus picked the first engagement `spine: true` instance (`firstWhere`), which after the multi-scenario corpus expansion lands on alpina-spine — whose events are inbox candidates (status: inbox), not processed history. So list_events is empty, the SOURCES filter renders nothing, and verify-demo.sh fails at `present sources-filter`. New `autoFocusTargetProvider` ranks the engagement-spine instances by processed-event count (list_events length) and focuses the most populated one (hoffmann-spine: 6 processed events), falling back to the first spine, then the first instance. Deterministic regardless of instance ordering. Test plan: - flutter test (94 widget tests) green; flutter analyze clean. - scripts/verify-demo.sh now exits 0: all 11 region labels present (incl. sources-filter), all /mcp probes pass, and the full M7 capture→inbox→agent-fold loop runs (spine events 6→7). Co-Authored-By: Claude Opus 4.8 --- .../lib/crm/crm_providers.dart | 30 +++++++++++++++++++ .../lib/crm/crm_workspace.dart | 15 ++++------ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/apps/escurel-explore/lib/crm/crm_providers.dart b/apps/escurel-explore/lib/crm/crm_providers.dart index 9a13184..f4b8057 100644 --- a/apps/escurel-explore/lib/crm/crm_providers.dart +++ b/apps/escurel-explore/lib/crm/crm_providers.dart @@ -25,6 +25,36 @@ final allInstancesProvider = FutureProvider>((ref) async { return out; }); +/// The instance the workspace auto-focuses on first load: the engagement +/// spine with the richest *processed* event history (the populated +/// showcase), falling back to the first spine, then the first instance. +/// `null` only when the tenant has no instances. +/// +/// Ranking by `list_events` length is deliberate: the corpus carries +/// several `spine: true` engagements but assigns *processed* history to +/// only one — the others' events sit in the inbox, so focusing them +/// would open an empty event view (no SOURCES filter, no timeline). A +/// few `list_events` calls on first load buy a deterministic landing on +/// the populated spine regardless of instance ordering. +final autoFocusTargetProvider = FutureProvider((ref) async { + final all = await ref.watch(allInstancesProvider.future); + if (all.isEmpty) return null; + final spines = + all.where((i) => i.skill == 'engagement' && i.id.contains('spine')).toList(); + if (spines.isEmpty) return all.first.id; + final client = ref.watch(escurelClientProvider); + var bestId = spines.first.id; + var bestCount = -1; + for (final s in spines) { + final count = (await client.listEvents(s.id)).length; + if (count > bestCount) { + bestCount = count; + bestId = s.id; + } + } + return bestId; +}); + /// Layout state for the resizable/collapsible two-pane split: the left /// pane's width fraction, and per-pane collapse flags. final leftPaneFractionProvider = StateProvider((ref) => 0.42); diff --git a/apps/escurel-explore/lib/crm/crm_workspace.dart b/apps/escurel-explore/lib/crm/crm_workspace.dart index 5b157fc..452fb61 100644 --- a/apps/escurel-explore/lib/crm/crm_workspace.dart +++ b/apps/escurel-explore/lib/crm/crm_workspace.dart @@ -31,18 +31,15 @@ class _CrmWorkspaceState extends ConsumerState { @override Widget build(BuildContext context) { - // Land on a populated view: auto-focus the engagement spine (else - // the first instance) once on first load. + // Land on a populated view: auto-focus the engagement spine with + // real processed event history (see autoFocusTargetProvider) once on + // first load. if (!_autoFocused && ref.watch(currentPageIdProvider) == null) { - ref.watch(allInstancesProvider).whenData((all) { - if (_autoFocused || all.isEmpty) return; + ref.watch(autoFocusTargetProvider).whenData((target) { + if (_autoFocused || target == null) return; _autoFocused = true; - final pick = all.firstWhere( - (i) => i.skill == 'engagement' && i.id.contains('spine'), - orElse: () => all.first, - ); WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) ref.read(currentPageIdProvider.notifier).state = pick.id; + if (mounted) ref.read(currentPageIdProvider.notifier).state = target; }); }); }