From 30a02ea95281043dee8924ee300f580015be63e8 Mon Sep 17 00:00:00 2001 From: Nikolaos Protopapas Date: Sat, 6 Jun 2026 01:47:17 +0300 Subject: [PATCH 1/2] Fix UI freeze when viewing read model instances MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Activating a read model row opened the detail overlay only after the instances fetch completed, blocking the Workbench UI thread on the network call (ReadModelsView.OpenDetailOverlay -> BuildInstancesContent -> OnFetchInstances(...).GetAwaiter().GetResult()). Open the overlay immediately with a "Loading instances…" placeholder, run the fetch off the UI thread (Task.Run + ConfigureAwait(false)), and push the result into the Instances tab on the UI thread via EnqueueOnUIThread. DetailOverlayWindow now exposes its per-tab editors so the content can be updated after Build. --- .../Workbench/views/ReadModelsView.cs | 35 ++++++++++++++++--- .../Workbench/windows/DetailOverlayWindow.cs | 8 +++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/Source/Cli/Commands/Chronicle/Workbench/views/ReadModelsView.cs b/Source/Cli/Commands/Chronicle/Workbench/views/ReadModelsView.cs index b304829..2a14b83 100644 --- a/Source/Cli/Commands/Chronicle/Workbench/views/ReadModelsView.cs +++ b/Source/Cli/Commands/Chronicle/Workbench/views/ReadModelsView.cs @@ -81,17 +81,42 @@ public void OpenDetailOverlay(WorkbenchReadModel rm) $"[{mut}]Queryable[/] [{queryableColor}]{(rm.IsQueryable ? "Yes" : "No")}[/]", $"[{mut}]Identifier[/] {rm.Identifier}"); - var instancesContent = BuildInstancesContent(rm); + // Open immediately with a placeholder for the Instances tab, then fetch off the UI thread so + // activating a row never blocks (or deadlocks) the render loop. The fetched content is pushed + // back into the tab editor on the UI thread once it arrives. + const string instancesTab = "Instances"; + var loadingContent = OnFetchInstances is null + ? $"[{mut}](No instance loader configured)[/]" + : $"[{mut}]Loading instances…[/]"; List<(string TabName, string Content)> tabs = [ ("Info", infoContent), - ("Instances", instancesContent) + (instancesTab, loadingContent) ]; var overlay = new DetailOverlayWindow(); var window = overlay.Build(_windowSystem, $" {rm.ContainerName} ", tabs, []); _windowSystem.AddWindow(window, activateWindow: true); + + if (OnFetchInstances is null) + { + return; + } + + var windowSystem = _windowSystem; + _ = Task.Run(async () => + { + var content = await FetchInstancesContentAsync(rm).ConfigureAwait(false); + windowSystem.EnqueueOnUIThread(() => + { + if (overlay.TabEditors.TryGetValue(instancesTab, out var editor)) + { + // The overlay strips markup to plain text for its read-only editors. + editor.SetContent(Markup.Remove(content)); + } + }); + }); } /// @@ -154,7 +179,7 @@ protected override IEnumerable GetCompletions(string input) => /// protected override void OnRowActivated(WorkbenchReadModel item) => OpenDetailOverlay(item); - string BuildInstancesContent(WorkbenchReadModel rm) + async Task FetchInstancesContentAsync(WorkbenchReadModel rm) { var mut = WorkbenchColors.Muted.ToMarkup(); var dan = WorkbenchColors.Danger.ToMarkup(); @@ -166,8 +191,8 @@ string BuildInstancesContent(WorkbenchReadModel rm) try { - var data = OnFetchInstances(rm.ContainerName, CancellationToken.None) - .GetAwaiter().GetResult(); + var data = await OnFetchInstances(rm.ContainerName, CancellationToken.None) + .ConfigureAwait(false); if (data.ReadModelInstancesError is not null) { diff --git a/Source/Cli/Commands/Chronicle/Workbench/windows/DetailOverlayWindow.cs b/Source/Cli/Commands/Chronicle/Workbench/windows/DetailOverlayWindow.cs index f0c7610..17d687e 100644 --- a/Source/Cli/Commands/Chronicle/Workbench/windows/DetailOverlayWindow.cs +++ b/Source/Cli/Commands/Chronicle/Workbench/windows/DetailOverlayWindow.cs @@ -13,6 +13,13 @@ namespace Cratis.Cli.Commands.Chronicle.Workbench; public class DetailOverlayWindow { Window? _window; + readonly Dictionary _tabEditors = []; + + /// + /// Gets the read-only editors backing each tab, keyed by tab name. Lets callers update a tab's + /// content after (for example, when a tab is populated by an async fetch). + /// + public IReadOnlyDictionary TabEditors => _tabEditors; /// /// Builds a detail overlay window with the specified title, tabbed content, and action buttons. @@ -49,6 +56,7 @@ public Window Build( .Build(); tabBuilder.AddTab(tabName, editor); + _tabEditors[tabName] = editor; } var tabControl = tabBuilder.Fill().Build(); From c41934222186a0ce90d2d3596bbbb85937c9d7bb Mon Sep 17 00:00:00 2001 From: woksin Date: Sun, 7 Jun 2026 14:29:19 +0200 Subject: [PATCH 2/2] Fix order of field declarations in DetailOverlayWindow class --- .../Commands/Chronicle/Workbench/windows/DetailOverlayWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Cli/Commands/Chronicle/Workbench/windows/DetailOverlayWindow.cs b/Source/Cli/Commands/Chronicle/Workbench/windows/DetailOverlayWindow.cs index 17d687e..4c09edb 100644 --- a/Source/Cli/Commands/Chronicle/Workbench/windows/DetailOverlayWindow.cs +++ b/Source/Cli/Commands/Chronicle/Workbench/windows/DetailOverlayWindow.cs @@ -12,8 +12,8 @@ namespace Cratis.Cli.Commands.Chronicle.Workbench; /// public class DetailOverlayWindow { - Window? _window; readonly Dictionary _tabEditors = []; + Window? _window; /// /// Gets the read-only editors backing each tab, keyed by tab name. Lets callers update a tab's