diff --git a/plugin-core/src/main/java/appland/index/AppMapFindingsUtil.java b/plugin-core/src/main/java/appland/index/AppMapFindingsUtil.java
index 58715071..9ea4338d 100644
--- a/plugin-core/src/main/java/appland/index/AppMapFindingsUtil.java
+++ b/plugin-core/src/main/java/appland/index/AppMapFindingsUtil.java
@@ -2,7 +2,10 @@
 
 import com.intellij.openapi.util.io.FileUtil;
 import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.search.FilenameIndex;
+import com.intellij.psi.search.GlobalSearchScope;
 import com.intellij.util.PathUtil;
+import com.intellij.util.concurrency.annotations.RequiresReadLock;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -19,4 +22,9 @@ public static boolean isFindingFile(@NotNull String path) {
     public static boolean isFindingFile(@Nullable VirtualFile file) {
         return file != null && FileUtil.fileNameEquals(file.getName(), FINDINGS_FILE_NAME);
     }
+
+    @RequiresReadLock
+    public static boolean isAnalysisPerformed(GlobalSearchScope searchScope) {
+        return !FilenameIndex.getVirtualFilesByName(FINDINGS_FILE_NAME, searchScope).isEmpty();
+    }
 }
diff --git a/plugin-core/src/main/java/appland/installGuide/InstallGuideEditor.java b/plugin-core/src/main/java/appland/installGuide/InstallGuideEditor.java
index 6276a40d..498f6890 100644
--- a/plugin-core/src/main/java/appland/installGuide/InstallGuideEditor.java
+++ b/plugin-core/src/main/java/appland/installGuide/InstallGuideEditor.java
@@ -8,11 +8,9 @@
 import appland.installGuide.projectData.ProjectDataService;
 import appland.installGuide.projectData.ProjectMetadata;
 import appland.oauth.AppMapLoginAction;
-import appland.problemsView.FindingsViewTab;
 import appland.settings.AppMapApplicationSettingsService;
 import appland.settings.AppMapProjectSettingsService;
 import appland.settings.AppMapSettingsListener;
-import appland.telemetry.TelemetryService;
 import appland.webviews.WebviewEditor;
 import appland.webviews.findings.FindingsOverviewEditorProvider;
 import com.google.gson.Gson;
@@ -68,14 +66,23 @@ public InstallGuideEditor(@NotNull Project project,
         this.currentPage = page;
     }
 
-    public void navigateTo(@NotNull InstallGuideViewPage page, boolean postWebviewMessage) {
+    /**
+     * Navigate to the given page.
+     *
+     * @param page               Target page
+     * @param postWebviewMessage If the webview should be instructed to navigate to the given page.
+     * @param fromWebview        If the navigation originated from the webview
+     */
+    public void navigateTo(@NotNull InstallGuideViewPage page, boolean postWebviewMessage, boolean fromWebview) {
         this.currentPage = page;
 
-        if (page == InstallGuideViewPage.RuntimeAnalysis) {
-            AppMapProjectSettingsService.getState(project).setInvestigatedFindings(true);
+        var projectSettings = AppMapProjectSettingsService.getState(project);
+        if (fromWebview && page == InstallGuideViewPage.RuntimeAnalysis && projectSettings.isOpenedAppMapEditor()) {
+            projectSettings.setInvestigatedFindings(true);
         }
 
         if (postWebviewMessage) {
+            assert !fromWebview;
             postMessage(createPageNavigationJSON(page));
         }
     }
@@ -217,7 +224,7 @@ protected void handleMessage(@NotNull String messageId, @Nullable JsonObject mes
 
     @NotNull
     private List<ProjectMetadata> findProjects() {
-        return ProjectDataService.getInstance(project).getAppMapProjects();
+        return ProjectDataService.getInstance(project).getAppMapProjects(true);
     }
 
     /**
@@ -275,7 +282,7 @@ private void handleMessageOpenFile(@NotNull JsonObject message) {
     private void handleMessageOpenPage(@NotNull JsonObject message) {
         // update state, which is based on the new page
         var viewId = message.getAsJsonPrimitive("page").getAsString();
-        navigateTo(InstallGuideViewPage.findByPageId(viewId), false);
+        navigateTo(InstallGuideViewPage.findByPageId(viewId), false, true);
     }
 
     private void executeInstallCommand(String path, String language) {
diff --git a/plugin-core/src/main/java/appland/installGuide/InstallGuideEditorProvider.java b/plugin-core/src/main/java/appland/installGuide/InstallGuideEditorProvider.java
index 8bbf64b5..acc51c10 100644
--- a/plugin-core/src/main/java/appland/installGuide/InstallGuideEditorProvider.java
+++ b/plugin-core/src/main/java/appland/installGuide/InstallGuideEditorProvider.java
@@ -35,7 +35,7 @@ public static void open(@NotNull Project project, @NotNull InstallGuideViewPage
                     assert editor instanceof InstallGuideEditor;
 
                     FileEditorManagerEx.getInstanceEx(project).openFile(file, true, true);
-                    ((InstallGuideEditor) editor).navigateTo(page, true);
+                    ((InstallGuideEditor) editor).navigateTo(page, true, false);
                     return;
                 }
             }
@@ -45,7 +45,7 @@ public static void open(@NotNull Project project, @NotNull InstallGuideViewPage
             var newEditors = editorManager.openFile(file, true);
             if (newEditors.length == 1) {
                 // don't post webview message because the init message of the new editor was just sent
-                ((InstallGuideEditor) newEditors[0]).navigateTo(page, false);
+                ((InstallGuideEditor) newEditors[0]).navigateTo(page, false, false);
             }
         } finally {
             // notify in a background thread because we don't want to delay opening the editor
diff --git a/plugin-core/src/main/java/appland/installGuide/projectData/DefaultProjectDataService.java b/plugin-core/src/main/java/appland/installGuide/projectData/DefaultProjectDataService.java
index 6a1a1528..9f6aa948 100644
--- a/plugin-core/src/main/java/appland/installGuide/projectData/DefaultProjectDataService.java
+++ b/plugin-core/src/main/java/appland/installGuide/projectData/DefaultProjectDataService.java
@@ -19,7 +19,6 @@
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VfsUtilCore;
 import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.psi.search.FilenameIndex;
 import com.intellij.psi.search.GlobalSearchScopes;
 import com.intellij.util.concurrency.annotations.RequiresReadLock;
 import it.unimi.dsi.fastutil.ints.IntSet;
@@ -47,10 +46,11 @@ public DefaultProjectDataService(@NotNull Project project) {
 
     @SuppressWarnings("UnstableApiUsage")
     @Override
-    public @NotNull List<ProjectMetadata> getAppMapProjects() {
-        ApplicationManager.getApplication().assertReadAccessNotAllowed();
-
-        updateMetadata();
+    public @NotNull List<ProjectMetadata> getAppMapProjects(boolean updateMetadata) {
+        if (updateMetadata) {
+            ApplicationManager.getApplication().assertReadAccessNotAllowed();
+            updateMetadata();
+        }
         return cachedProjects.get();
     }
 
@@ -203,7 +203,7 @@ private List<SimpleCodeObject> truncateSampleCodeObjects(List<SimpleCodeObject>
     @RequiresReadLock
     private boolean isAnalysisPerformed(@NotNull VirtualFile root) {
         var searchScope = GlobalSearchScopes.directoryScope(project, root, true);
-        return !FilenameIndex.getVirtualFilesByName(AppMapFindingsUtil.FINDINGS_FILE_NAME, searchScope).isEmpty();
+        return AppMapFindingsUtil.isAnalysisPerformed(searchScope);
     }
 
     private boolean isNodeSupported(@Nullable NodeVersion nodeVersion) {
diff --git a/plugin-core/src/main/java/appland/installGuide/projectData/ProjectDataService.java b/plugin-core/src/main/java/appland/installGuide/projectData/ProjectDataService.java
index 6c087476..81f03c19 100644
--- a/plugin-core/src/main/java/appland/installGuide/projectData/ProjectDataService.java
+++ b/plugin-core/src/main/java/appland/installGuide/projectData/ProjectDataService.java
@@ -10,5 +10,10 @@ public interface ProjectDataService {
         return project.getService(ProjectDataService.class);
     }
 
-    @NotNull List<ProjectMetadata> getAppMapProjects();
+    /**
+     * @param updateMetadata If the metadata should be updated before retuning the cached projects.
+     *                       If {@code true}, then this method MUST NOT be invoked in a ReadAction.
+     * @return The available AppMap projects
+     */
+    @NotNull List<ProjectMetadata> getAppMapProjects(boolean updateMetadata);
 }
diff --git a/plugin-core/src/main/java/appland/toolwindow/installGuide/InstallGuidePanel.java b/plugin-core/src/main/java/appland/toolwindow/installGuide/InstallGuidePanel.java
index 33536f96..b33e7741 100644
--- a/plugin-core/src/main/java/appland/toolwindow/installGuide/InstallGuidePanel.java
+++ b/plugin-core/src/main/java/appland/toolwindow/installGuide/InstallGuidePanel.java
@@ -1,6 +1,7 @@
 package appland.toolwindow.installGuide;
 
 import appland.config.AppMapConfigFileListener;
+import appland.index.AppMapFindingsUtil;
 import appland.index.AppMapNameIndex;
 import appland.index.AppMapSearchScopes;
 import appland.installGuide.InstallGuideViewPage;
@@ -11,7 +12,6 @@
 import appland.settings.AppMapSettingsListener;
 import appland.toolwindow.AppMapContentPanel;
 import com.intellij.openapi.Disposable;
-import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.application.ModalityState;
 import com.intellij.openapi.application.ReadAction;
 import com.intellij.openapi.project.Project;
@@ -19,10 +19,12 @@
 import com.intellij.util.Alarm;
 import com.intellij.util.SingleAlarm;
 import com.intellij.util.concurrency.AppExecutorUtil;
+import com.intellij.util.concurrency.annotations.RequiresBackgroundThread;
 import org.jetbrains.annotations.NotNull;
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.function.Function;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
@@ -35,7 +37,7 @@ public class InstallGuidePanel extends AppMapContentPanel implements Disposable
     private final Project project;
     private final List<StatusLabel> statusLabels;
     // debounce label updates by 500ms
-    private final SingleAlarm labelRefreshAlarm = new SingleAlarm(this::refreshInitialStatus, 500, this, Alarm.ThreadToUse.SWING_THREAD);
+    private final SingleAlarm labelRefreshAlarm = new SingleAlarm(this::refreshInitialStatus, 500, this, Alarm.ThreadToUse.POOLED_THREAD);
 
     public InstallGuidePanel(@NotNull Project project, @NotNull Disposable parent) {
         super(false);
@@ -48,92 +50,107 @@ public InstallGuidePanel(@NotNull Project project, @NotNull Disposable parent) {
         setupPanel();
     }
 
+    @Override
+    public void dispose() {
+    }
+
     @Override
     protected void setupPanel() {
         statusLabels.forEach(this::add);
-        refreshInitialStatus();
         registerStatusUpdateListeners(project, this, statusLabels);
+        labelRefreshAlarm.request(true);
     }
 
     private void triggerLabelStatusUpdate() {
         labelRefreshAlarm.cancelAndRequest();
     }
 
+    @RequiresBackgroundThread
     private void refreshInitialStatus() {
-        for (var label : statusLabels) {
-            updateLabelStatus(project, label);
-        }
+        // Force a refresh of the metadata outside the non-blocking ReadAction below
+        // The refresh must not be executed within a ReadAction.
+        ProjectDataService.getInstance(project).getAppMapProjects(true);
+
+        ReadAction.nonBlocking(() -> {
+                    return statusLabels.stream().collect(Collectors.toMap(Function.identity(), label -> {
+                        return updateLabelStatus(project, label);
+                    }));
+                })
+                .coalesceBy(this)
+                .expireWith(this)
+                .inSmartMode(project)
+                .finishOnUiThread(ModalityState.any(), statusMapping -> {
+                            for (var entry : statusMapping.entrySet()) {
+                                entry.getKey().setStatus(entry.getValue());
+                            }
+                        }
+                )
+                .submit(AppExecutorUtil.getAppExecutorService());
     }
 
     /**
-     * if findings are enabled, completed if at least one appmap-findings.json file was found
+     * @param project Current project
+     * @param label   Label to check
+     * @return {@code true} if the label should be enabled, {@code false} if it should be disabled
      */
-    private static void updateRuntimeAnalysisLabel(@NotNull Project project, @NotNull StatusLabel label) {
-        var hasFindings = AppMapProjectSettingsService.getState(project).isInvestigatedFindings();
-        label.setStatus(hasFindings ? InstallGuideStatus.Completed : InstallGuideStatus.Incomplete);
-    }
-
-    private void updateLabelStatus(@NotNull Project project, @NotNull StatusLabel label) {
+    private @NotNull InstallGuideStatus updateLabelStatus(@NotNull Project project, @NotNull StatusLabel label) {
         switch (label.getPage()) {
             case InstallAgent:
-                updateInstallAgentLabel(project, label);
-                break;
-
+                return updateInstallAgentLabel(project);
             case RecordAppMaps:
-                updateRecordAppMapsLabel(project, label);
-                break;
-
+                return updateRecordAppMapsLabel(project);
             case OpenAppMaps:
-                updateOpenAppMapsLabel(project, label);
-                break;
-
+                return updateOpenAppMapsLabel(project);
             case RuntimeAnalysis:
-                updateRuntimeAnalysisLabel(project, label);
-                break;
+                return updateRuntimeAnalysisLabel(project);
+            default:
+                throw new IllegalStateException("unexpected label: " + label);
         }
     }
 
-    @Override
-    public void dispose() {
-    }
-
     /**
      * Presence of at least one appmap.yml file.
      */
-    private static void updateInstallAgentLabel(@NotNull Project project, @NotNull StatusLabel label) {
-        ApplicationManager.getApplication().executeOnPooledThread(() -> {
-            // Always mark supported Java projects as supported, but only if all modules are supported.
-            // isAgentInstalled is already checking for supported Java projects and the presence of appmap.yml.
-            var anySupported = ProjectDataService.getInstance(project).getAppMapProjects()
-                    .stream()
-                    .anyMatch(ProjectMetadata::isAgentInstalled);
-
-            ApplicationManager.getApplication().invokeLater(() -> {
-                label.setStatus(anySupported ? InstallGuideStatus.Completed : InstallGuideStatus.Incomplete);
-            });
-        });
+    private static @NotNull InstallGuideStatus updateInstallAgentLabel(@NotNull Project project) {
+        // Always mark supported Java projects as supported, but only if all modules are supported.
+        // isAgentInstalled is already checking for supported Java projects and the presence of appmap.yml.
+        var anySupported = ProjectDataService.getInstance(project).getAppMapProjects(false)
+                .stream()
+                .anyMatch(ProjectMetadata::isAgentInstalled);
+        return anySupported ? InstallGuideStatus.Completed : InstallGuideStatus.Incomplete;
     }
 
     /**
-     * presence of at least one .appmap.json file
+     * Presence of at least one .appmap.json file.
      */
-    private static void updateRecordAppMapsLabel(@NotNull Project project, @NotNull StatusLabel label) {
-        findIndexedStatus(project, label, () -> !AppMapNameIndex.isEmpty(project, AppMapSearchScopes.appMapsWithExcluded(project)));
+    private static @NotNull InstallGuideStatus updateRecordAppMapsLabel(@NotNull Project project) {
+        return !AppMapNameIndex.isEmpty(project, AppMapSearchScopes.appMapsWithExcluded(project))
+                ? InstallGuideStatus.Completed
+                : InstallGuideStatus.Incomplete;
     }
 
     /**
-     * if the AppMap webview was at least shown once
+     * If the AppMap webview was at least shown once
      */
-    private static void updateOpenAppMapsLabel(@NotNull Project project, @NotNull StatusLabel label) {
-        label.setStatus(AppMapProjectSettingsService.getState(project).isOpenedAppMapEditor()
+    private static @NotNull InstallGuideStatus updateOpenAppMapsLabel(@NotNull Project project) {
+        return AppMapProjectSettingsService.getState(project).isOpenedAppMapEditor()
                 ? InstallGuideStatus.Completed
-                : InstallGuideStatus.Incomplete);
+                : InstallGuideStatus.Incomplete;
     }
 
-    private static void updateGenerateOpenApiLabel(@NotNull Project project, @NotNull StatusLabel label) {
-        label.setStatus(AppMapProjectSettingsService.getState(project).isCreatedOpenAPI()
+    /**
+     * The label is updated under the following conditions:
+     * - The user navigated to the "Runtime Analysis" step in the installation guide webview
+     * - AppMaps have been opened
+     * - There are AppMaps in the project, and they have been scanned, regardless if findings were detected
+     */
+    private static @NotNull InstallGuideStatus updateRuntimeAnalysisLabel(@NotNull Project project) {
+        var navigated = AppMapProjectSettingsService.getState(project).isInvestigatedFindings();
+        var openedAppMaps = AppMapProjectSettingsService.getState(project).isOpenedAppMapEditor();
+        var scannedAppMaps = AppMapFindingsUtil.isAnalysisPerformed(AppMapSearchScopes.projectFilesWithExcluded(project));
+        return navigated && openedAppMaps && scannedAppMaps
                 ? InstallGuideStatus.Completed
-                : InstallGuideStatus.Incomplete);
+                : InstallGuideStatus.Incomplete;
     }
 
     private void registerStatusUpdateListeners(@NotNull Project project,
diff --git a/plugin-core/src/test/java/appland/installGuide/VSCodeFixturesTest.java b/plugin-core/src/test/java/appland/installGuide/VSCodeFixturesTest.java
index 5d901e81..c5c1b85d 100644
--- a/plugin-core/src/test/java/appland/installGuide/VSCodeFixturesTest.java
+++ b/plugin-core/src/test/java/appland/installGuide/VSCodeFixturesTest.java
@@ -22,7 +22,7 @@ public void projectA() {
         // copy into src, which is the only content root of the test project
         WriteAction.runAndWait(() -> myFixture.copyDirectoryToProject("project-a", ""));
 
-        var projects = ProjectDataService.getInstance(getProject()).getAppMapProjects();
+        var projects = ProjectDataService.getInstance(getProject()).getAppMapProjects(true);
         Assert.assertEquals(1, projects.size());
 
         var projectA = projects.get(0);
diff --git a/plugin-core/src/test/java/appland/installGuide/projectData/DefaultProjectDataServiceTest.java b/plugin-core/src/test/java/appland/installGuide/projectData/DefaultProjectDataServiceTest.java
index 3d1d4e0f..73a33337 100644
--- a/plugin-core/src/test/java/appland/installGuide/projectData/DefaultProjectDataServiceTest.java
+++ b/plugin-core/src/test/java/appland/installGuide/projectData/DefaultProjectDataServiceTest.java
@@ -14,7 +14,7 @@ protected boolean runInDispatchThread() {
     public void selectedSampleObjects() {
         WriteAction.runAndWait(() -> myFixture.copyDirectoryToProject("appmap-scanner/with_findings", "root"));
 
-        var projects = ProjectDataService.getInstance(getProject()).getAppMapProjects();
+        var projects = ProjectDataService.getInstance(getProject()).getAppMapProjects(true);
         assertEquals(1, projects.size());
 
         var samples = projects.get(0).sampleCodeObjects;
@@ -43,7 +43,7 @@ public void selectedSampleObjects() {
     public void truncatedSampleObjects() {
         WriteAction.runAndWait(() -> myFixture.copyDirectoryToProject("appmap-scanner/untruncated_findings", "root"));
 
-        var projects = ProjectDataService.getInstance(getProject()).getAppMapProjects();
+        var projects = ProjectDataService.getInstance(getProject()).getAppMapProjects(true);
         assertEquals(1, projects.size());
 
         var samples = projects.get(0).sampleCodeObjects;