Disable screens not compatible with DWDS websocket mode (#9481)

The DWDS websocket mode used by the Flutter `web-server` device provides
a limited subset of the service protocol that doesn't include debugging
capabilities, expression evaluation support, or object inspection. This
change adds detection for applications running in this limited mode and
hides screens that rely on unsupported functionality (e.g., debugger).
diff --git a/packages/devtools_app/lib/src/shared/framework/screen.dart b/packages/devtools_app/lib/src/shared/framework/screen.dart
index 29cb627..c3b4931 100644
--- a/packages/devtools_app/lib/src/shared/framework/screen.dart
+++ b/packages/devtools_app/lib/src/shared/framework/screen.dart
@@ -32,6 +32,7 @@
     'home',
     iconAsset: 'icons/app_bar/devtools.png',
     requiresConnection: false,
+    supportsWebServerDevice: true,
     tutorialVideoTimestamp: '?t=0',
   ),
   inspector(
@@ -40,6 +41,7 @@
     iconAsset: 'icons/app_bar/inspector.png',
     requiresFlutter: true,
     requiresDebugBuild: true,
+    supportsWebServerDevice: true,
     tutorialVideoTimestamp: '?t=172',
   ),
   performance(
@@ -89,6 +91,7 @@
     'logging',
     title: 'Logging',
     iconAsset: 'icons/app_bar/logging.png',
+    supportsWebServerDevice: true,
     tutorialVideoTimestamp: '?t=558',
   ),
   provider(
@@ -138,6 +141,7 @@
     this.requiresFlutter = false,
     this.requiresDebugBuild = false,
     this.requiresAdvancedDeveloperMode = false,
+    this.supportsWebServerDevice = false,
     this.worksWithOfflineData = false,
     this.requiresLibrary,
     this.tutorialVideoTimestamp,
@@ -155,6 +159,7 @@
   final bool requiresFlutter;
   final bool requiresDebugBuild;
   final bool requiresAdvancedDeveloperMode;
+  final bool supportsWebServerDevice;
   final bool worksWithOfflineData;
   final String? requiresLibrary;
 
@@ -204,6 +209,7 @@
     this.requiresFlutter = false,
     this.requiresDebugBuild = false,
     this.requiresAdvancedDeveloperMode = false,
+    this.supportsWebServerDevice = false,
     this.worksWithOfflineData = false,
     this.showFloatingDebuggerControls = true,
   }) : assert(
@@ -223,6 +229,7 @@
     bool requiresFlutter = false,
     bool requiresDebugBuild = false,
     bool requiresAdvancedDeveloperMode = false,
+    bool supportsWebServerDevice = false,
     bool worksWithOfflineData = false,
     bool Function(FlutterVersion? currentVersion)? shouldShowForFlutterVersion,
     bool showFloatingDebuggerControls = true,
@@ -239,6 +246,7 @@
          requiresFlutter: requiresFlutter,
          requiresDebugBuild: requiresDebugBuild,
          requiresAdvancedDeveloperMode: requiresAdvancedDeveloperMode,
+         supportsWebServerDevice: supportsWebServerDevice,
          worksWithOfflineData: worksWithOfflineData,
          showFloatingDebuggerControls: showFloatingDebuggerControls,
          title: title,
@@ -262,6 +270,7 @@
          requiresFlutter: metadata.requiresFlutter,
          requiresDebugBuild: metadata.requiresDebugBuild,
          requiresAdvancedDeveloperMode: metadata.requiresAdvancedDeveloperMode,
+         supportsWebServerDevice: metadata.supportsWebServerDevice,
          worksWithOfflineData: metadata.worksWithOfflineData,
          shouldShowForFlutterVersion: shouldShowForFlutterVersion,
          showFloatingDebuggerControls: showFloatingDebuggerControls,
@@ -339,6 +348,10 @@
   /// is enabled.
   final bool requiresAdvancedDeveloperMode;
 
+  /// Whether this screen should be included when the app is a web app without full debugging
+  /// support.
+  final bool supportsWebServerDevice;
+
   /// Whether this screen works offline and should show in offline mode even if conditions are not met.
   final bool worksWithOfflineData;
 
@@ -490,9 +503,11 @@
     }
   }
 
+  final serviceManager = serviceConnection.serviceManager;
+  final connectedApp = serviceManager.connectedApp;
   final serviceReady =
-      serviceConnection.serviceManager.isServiceAvailable &&
-      serviceConnection.serviceManager.connectedApp!.connectedAppInitialized;
+      serviceManager.isServiceAvailable &&
+      connectedApp!.connectedAppInitialized;
   if (!serviceReady) {
     if (!screen.requiresConnection) {
       _log.finest('screen does not require connection: returning true');
@@ -509,42 +524,42 @@
     }
   }
 
-  if (screen.requiresLibrary != null) {
-    if (serviceConnection.serviceManager.isolateManager.mainIsolate.value ==
-            null ||
-        !serviceConnection.serviceManager.libraryUriAvailableNow(
-          screen.requiresLibrary,
-        )) {
-      _log.finest(
-        'screen requires library ${screen.requiresLibrary}: returning false',
-      );
-      return (
-        show: false,
-        disabledReason: ScreenDisabledReason.requiresDartLibrary,
-      );
-    }
+  if (screen.requiresLibrary != null &&
+      (serviceManager.isolateManager.mainIsolate.value == null ||
+          !serviceManager.libraryUriAvailableNow(screen.requiresLibrary))) {
+    _log.finest(
+      'screen requires library ${screen.requiresLibrary}: returning false',
+    );
+    return (
+      show: false,
+      disabledReason: ScreenDisabledReason.requiresDartLibrary,
+    );
   }
-  if (screen.requiresDartVm) {
-    if (serviceConnection.serviceManager.connectedApp!.isRunningOnDartVM !=
-        true) {
-      _log.finest('screen requires Dart VM: returning false');
-      return (show: false, disabledReason: ScreenDisabledReason.requiresDartVm);
-    }
+  if (screen.requiresDartVm && connectedApp.isRunningOnDartVM != true) {
+    _log.finest('screen requires Dart VM: returning false');
+    return (show: false, disabledReason: ScreenDisabledReason.requiresDartVm);
   }
-  if (screen.requiresFlutter &&
-      serviceConnection.serviceManager.connectedApp!.isFlutterAppNow == false) {
+  if (screen.requiresFlutter && connectedApp.isFlutterAppNow == false) {
     _log.finest('screen requires Flutter: returning false');
     return (show: false, disabledReason: ScreenDisabledReason.requiresFlutter);
   }
-  if (screen.requiresDebugBuild) {
-    if (serviceConnection.serviceManager.connectedApp!.isProfileBuildNow ==
-        true) {
-      _log.finest('screen requires debug build: returning false');
-      return (
-        show: false,
-        disabledReason: ScreenDisabledReason.requiresDebugBuild,
-      );
-    }
+  if (screen.requiresDebugBuild && connectedApp.isProfileBuildNow == true) {
+    _log.finest('screen requires debug build: returning false');
+    return (
+      show: false,
+      disabledReason: ScreenDisabledReason.requiresDebugBuild,
+    );
+  }
+  if (!screen.supportsWebServerDevice &&
+      connectedApp.isDartWebAppNow == true &&
+      !connectedApp.isDebuggableWebApp) {
+    _log.finest(
+      'screen requires a debuggable web application: returning false',
+    );
+    return (
+      show: false,
+      disabledReason: ScreenDisabledReason.requiresDebuggableWebApp,
+    );
   }
   _log.finest('${screen.screenId} screen supported: returning true');
   return (show: true, disabledReason: null);
@@ -573,6 +588,9 @@
   requiresAdvancedDeveloperMode(
     'only works when Advanced Developer Mode is enabled',
   ),
+  requiresDebuggableWebApp(
+    'only works with web applications with full debugging support.',
+  ),
   serviceNotReady(
     'requires a connected application, but there is no connection available.',
   );
diff --git a/packages/devtools_app/test/shared/framework/visible_screens_test.dart b/packages/devtools_app/test/shared/framework/visible_screens_test.dart
index 4e1a97c..4cc7425 100644
--- a/packages/devtools_app/test/shared/framework/visible_screens_test.dart
+++ b/packages/devtools_app/test/shared/framework/visible_screens_test.dart
@@ -41,6 +41,7 @@
 
     void setupMockConnectedApp({
       bool web = false,
+      bool debuggableWeb = true,
       bool flutter = false,
       bool debugMode = true,
       SemanticVersion? flutterVersion,
@@ -55,6 +56,7 @@
         isFlutterApp: flutter,
         isProfileBuild: !debugMode,
         isWebApp: web,
+        isDebuggableWebApp: debuggableWeb,
       );
       if (flutter) {
         fakeServiceConnection.serviceManager.availableLibraries.add(
@@ -114,6 +116,31 @@
       );
     });
 
+    testWidgets('are correct for Dart Web app (DWDS websocket mode)', (
+      WidgetTester tester,
+    ) async {
+      setupMockConnectedApp(web: true, debuggableWeb: false);
+
+      expect(
+        visibleScreenTypes,
+        equals([
+          HomeScreen,
+          // InspectorScreen,
+          // LegacyPerformanceScreen,
+          // PerformanceScreen,
+          // ProfilerScreen,
+          // MemoryScreen,
+          // DebuggerScreen,
+          // NetworkScreen,
+          LoggingScreen,
+          // AppSizeScreen,
+          // DeepLinksScreen,
+          // VMDeveloperToolsScreen,
+          // DTDToolsScreen,
+        ]),
+      );
+    });
+
     testWidgets('are correct for Flutter (non-web) debug app', (
       WidgetTester tester,
     ) async {
@@ -189,6 +216,31 @@
       );
     });
 
+    testWidgets('are correct for Flutter web debug app (DWDS websocket mode)', (
+      WidgetTester tester,
+    ) async {
+      setupMockConnectedApp(flutter: true, web: true, debuggableWeb: false);
+
+      expect(
+        visibleScreenTypes,
+        equals([
+          HomeScreen,
+          InspectorScreen,
+          // LegacyPerformanceScreen,
+          // PerformanceScreen,
+          // ProfilerScreen,
+          // MemoryScreen,
+          // DebuggerScreen,
+          // NetworkScreen,
+          LoggingScreen,
+          // AppSizeScreen,
+          // DeepLinksScreen,
+          // VMDeveloperToolsScreen,
+          // DTDToolsScreen,
+        ]),
+      );
+    });
+
     testWidgets('are correct for Flutter app on old Flutter version', (
       WidgetTester tester,
     ) async {
diff --git a/packages/devtools_app_shared/lib/src/service/connected_app.dart b/packages/devtools_app_shared/lib/src/service/connected_app.dart
index 17a625e..39f1fc2 100644
--- a/packages/devtools_app_shared/lib/src/service/connected_app.dart
+++ b/packages/devtools_app_shared/lib/src/service/connected_app.dart
@@ -101,6 +101,16 @@
 
   bool get isDebugFlutterAppNow => isFlutterAppNow! && !isProfileBuildNow!;
 
+  /// Returns true is the connected application's VM has the name
+  /// [dwdsChromeDebugProxyDeviceName], indicating that DWDS has an active
+  /// Chrome debugger connection with support for expression evaluation,
+  /// object inspection, and setting breakpoints.
+  ///
+  /// If false, the connected application supports a reduced subset of the VM
+  /// service protocol.
+  bool get isDebuggableWebApp =>
+      serviceManager!.vm!.name == dwdsChromeDebugProxyDeviceName;
+
   bool? get isRunningOnDartVM {
     final name = serviceManager!.vm!.name;
     // These are the two possible VM names returned by DWDS.
@@ -133,24 +143,6 @@
       shouldLogError: false,
     );
     return !(value?.kind == 'Bool');
-
-    // TODO(terry): Disabled below code, it will hang if flutter run --start-paused
-    //              see issue https://github.com/flutter/devtools/issues/2082.
-    //              Currently, if eval (see above) doesn't work then we're
-    //              running in Profile mode.
-    /*
-    assert(serviceConnectionManager.isServiceAvailable);
-    // Only flutter apps have profile and non-profile builds. If this changes in
-    // the future (flutter web), we can modify this check.
-    if (!isRunningOnDartVM || !await isFlutterApp) return false;
-
-    await serviceConnectionManager.manager.serviceExtensionManager.extensionStatesUpdated.future;
-
-    // The debugAllowBanner extension is only available in debug builds
-    final hasDebugExtension = serviceConnectionManager.manager.serviceExtensionManager
-        .isServiceExtensionAvailable(extensions.debugAllowBanner.extension);
-    return !hasDebugExtension;
-    */
   }
 
   Future<void> initializeValues({void Function()? onComplete}) async {
diff --git a/packages/devtools_test/lib/src/mocks/mocks.dart b/packages/devtools_test/lib/src/mocks/mocks.dart
index d2f2b10..fee470e 100644
--- a/packages/devtools_test/lib/src/mocks/mocks.dart
+++ b/packages/devtools_test/lib/src/mocks/mocks.dart
@@ -146,6 +146,7 @@
   bool isFlutterApp = true,
   bool isProfileBuild = false,
   bool isWebApp = false,
+  bool isDebuggableWebApp = true,
   String os = 'ios',
   String flutterVersion = '2.10.0',
 }) {
@@ -160,6 +161,9 @@
   when(
     connectedApp.isFlutterNativeAppNow,
   ).thenReturn(isFlutterApp && !isWebApp);
+  when(
+    connectedApp.isDebuggableWebApp,
+  ).thenReturn(isWebApp && isDebuggableWebApp);
   if (isFlutterApp) {
     when(connectedApp.flutterVersionNow).thenReturn(
       FlutterVersion.parse({