Integrate DTD with the Simulated DevTools Environment (#7133)

diff --git a/packages/.vscode/launch.json b/packages/.vscode/launch.json
index b6f51bc..3e537d8 100644
--- a/packages/.vscode/launch.json
+++ b/packages/.vscode/launch.json
@@ -119,5 +119,14 @@
             "type": "dart",
             "request": "attach",
         },
+        {
+            "name": "foo_devtools_extension example + simulated environment",
+            "request": "launch",
+            "type": "dart",
+            "program": "devtools_extensions/example/foo/packages/foo_devtools_extension/lib/main.dart",
+            "args": [
+                "--dart-define=use_simulated_environment=true"
+            ],
+        },
     ]
 }
diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
index 98787fb..95e563c 100644
--- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
+++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
@@ -66,6 +66,10 @@
 
 ## DevTools Extension updates
 
+* Added support for connecting to the Dart Tooling Daemon from the 
+simulated DevTools environment. - [#7133](https://github.com/flutter/devtools/pull/7133)
+* Added help buttons to the VM Service and DTD connection text fields in the
+simulated DevTools environment. - [#7133](https://github.com/flutter/devtools/pull/7133)
 * Fixed an issue with not detecting extensions for test files in
 subdirectories. - [#7174](https://github.com/flutter/devtools/pull/7174)
 * Added an example of creating an extension for a pure Dart package. - [#7196](https://github.com/flutter/devtools/pull/7196)
diff --git a/packages/devtools_extensions/CHANGELOG.md b/packages/devtools_extensions/CHANGELOG.md
index b5be606..7cab68a 100644
--- a/packages/devtools_extensions/CHANGELOG.md
+++ b/packages/devtools_extensions/CHANGELOG.md
@@ -1,5 +1,9 @@
 ## 0.0.14-wip
 * Add a global `dtdManager` for interacting with the Dart Tooling Daemon.
+* Add support for connecting to the Dart Tooling Daemon from the 
+simulated DevTools environment.
+* Add help buttons to the VM Service and DTD connection text fields in the
+simulated DevTools environment.
 * Bump `package:vm_service` dependency to ^14.0.0.
 * Refactor `example` directory to support more package examples.
 * Add an example of providing an extension from a pure Dart package.
diff --git a/packages/devtools_extensions/integration_test/test/simulated_environment_test.dart b/packages/devtools_extensions/integration_test/test/simulated_environment_test.dart
index d5272ee..0fe030d 100644
--- a/packages/devtools_extensions/integration_test/test/simulated_environment_test.dart
+++ b/packages/devtools_extensions/integration_test/test/simulated_environment_test.dart
@@ -214,7 +214,7 @@
   // Check that the [environmentPanelSizedBoxWidth] is the expected width.
   expect(
     environmentPanelSizedBoxWidth,
-    VmServiceConnection.totalControlsWidth + 2 * defaultSpacing,
+    VmServiceConnectionDisplay.totalControlsWidth + 2 * defaultSpacing,
   );
 
   // Drag the divider to the right by [environmentPanelSizedBoxWidth].
diff --git a/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_connect_ui.dart b/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_connect_ui.dart
deleted file mode 100644
index 69d0ff8..0000000
--- a/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_connect_ui.dart
+++ /dev/null
@@ -1,142 +0,0 @@
-// Copyright 2023 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-part of '_simulated_devtools_environment.dart';
-
-@visibleForTesting
-class VmServiceConnection extends StatelessWidget {
-  const VmServiceConnection({
-    super.key,
-    required this.simController,
-    required this.connected,
-  });
-
-  @visibleForTesting
-  static const totalControlsHeight = 45.0;
-  @visibleForTesting
-  static const totalControlsWidth = 415.0;
-
-  final SimulatedDevToolsController simController;
-  final bool connected;
-
-  @override
-  Widget build(BuildContext context) {
-    return SizedBox(
-      height: totalControlsHeight,
-      child: connected
-          ? _ConnectedVmServiceDisplay(simController: simController)
-          : _DisconnectedVmServiceDisplay(simController: simController),
-    );
-  }
-}
-
-class _ConnectedVmServiceDisplay extends StatelessWidget {
-  const _ConnectedVmServiceDisplay({required this.simController});
-
-  final SimulatedDevToolsController simController;
-
-  @override
-  Widget build(BuildContext context) {
-    final theme = Theme.of(context);
-    return Row(
-      mainAxisSize: MainAxisSize.min,
-      children: [
-        Column(
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            Text(
-              'Debugging:',
-              style: theme.regularTextStyle,
-            ),
-            Text(
-              serviceManager.service!.wsUri ?? '--',
-              style: theme.boldTextStyle,
-            ),
-          ],
-        ),
-        const Expanded(
-          child: SizedBox(width: denseSpacing),
-        ),
-        DevToolsButton(
-          elevated: true,
-          label: 'Disconnect',
-          onPressed: () {
-            simController.updateVmServiceConnection(uri: null);
-          },
-        ),
-      ],
-    );
-  }
-}
-
-class _DisconnectedVmServiceDisplay extends StatefulWidget {
-  const _DisconnectedVmServiceDisplay({required this.simController});
-
-  final SimulatedDevToolsController simController;
-
-  @override
-  State<_DisconnectedVmServiceDisplay> createState() =>
-      _DisconnectedVmServiceDisplayState();
-}
-
-class _DisconnectedVmServiceDisplayState
-    extends State<_DisconnectedVmServiceDisplay> {
-  static const _connectFieldWidth = 300.0;
-
-  late final TextEditingController _connectTextFieldController;
-
-  @override
-  void initState() {
-    super.initState();
-    _connectTextFieldController = TextEditingController();
-  }
-
-  @override
-  void dispose() {
-    _connectTextFieldController.dispose();
-    super.dispose();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    final theme = Theme.of(context);
-    return Row(
-      mainAxisSize: MainAxisSize.min,
-      children: [
-        SizedBox(
-          width: _connectFieldWidth,
-          child: TextField(
-            autofocus: true,
-            style: theme.regularTextStyle,
-            decoration: InputDecoration(
-              contentPadding: const EdgeInsets.all(denseSpacing),
-              isDense: true,
-              border: const OutlineInputBorder(),
-              enabledBorder: OutlineInputBorder(
-                borderSide: BorderSide(width: 0.5, color: theme.focusColor),
-              ),
-              labelText: 'Dart VM Service URL',
-              labelStyle: theme.regularTextStyle,
-              hintText: '(e.g., http://127.0.0.1:60851/fH-kAEXc7MQ=/)',
-              hintStyle: theme.regularTextStyle,
-            ),
-            onSubmitted: (value) =>
-                widget.simController.updateVmServiceConnection(uri: value),
-            controller: _connectTextFieldController,
-          ),
-        ),
-        const SizedBox(width: denseSpacing),
-        DevToolsButton(
-          elevated: true,
-          label: 'Connect',
-          onPressed: () {
-            widget.simController.updateVmServiceConnection(
-              uri: _connectTextFieldController.text,
-            );
-          },
-        ),
-      ],
-    );
-  }
-}
diff --git a/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_simulated_devtools_controller.dart b/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_simulated_devtools_controller.dart
index df34f80..1068e44 100644
--- a/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_simulated_devtools_controller.dart
+++ b/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_simulated_devtools_controller.dart
@@ -117,22 +117,12 @@
 
   Future<void> hotReloadConnectedApp() async {
     await serviceManager.performHotReload();
-    messageLogs.add(
-      MessageLogEntry(
-        source: MessageSource.info,
-        message: 'Hot reload performed on connected app',
-      ),
-    );
+    logInfoEvent('Hot reload performed on connected app');
   }
 
   Future<void> hotRestartConnectedApp() async {
     await serviceManager.performHotRestart();
-    messageLogs.add(
-      MessageLogEntry(
-        source: MessageSource.info,
-        message: 'Hot restart performed on connected app',
-      ),
-    );
+    logInfoEvent('Hot restart performed on connected app');
   }
 
   void toggleTheme() {
@@ -144,6 +134,12 @@
     );
   }
 
+  void logInfoEvent(String message) {
+    messageLogs.add(
+      MessageLogEntry(source: MessageSource.info, message: message),
+    );
+  }
+
   @override
   void forceReload() {
     _postMessageToExtension(
diff --git a/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_simulated_devtools_environment.dart b/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_simulated_devtools_environment.dart
index 2d9faa9..86d6d61 100644
--- a/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_simulated_devtools_environment.dart
+++ b/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/_simulated_devtools_environment.dart
@@ -10,15 +10,18 @@
 import 'package:devtools_app_shared/ui.dart';
 import 'package:devtools_app_shared/utils.dart';
 import 'package:flutter/material.dart';
-import 'package:web/web.dart' hide Text;
+import 'package:flutter/services.dart';
+import 'package:web/web.dart' hide Text, Clipboard;
 
 import '../../api/api.dart';
 import '../../api/model.dart';
 import '../../utils.dart';
 import '../devtools_extension.dart';
 
-part '_connect_ui.dart';
 part '_simulated_devtools_controller.dart';
+part 'connection_ui/_connect.dart';
+part 'connection_ui/_dtd_connect.dart';
+part 'connection_ui/_vm_service_connect.dart';
 
 /// Wraps [child] in a simulated DevTools environment.
 ///
@@ -33,12 +36,15 @@
     super.key,
     required this.child,
     required this.requiresRunningApplication,
+    required this.onDtdConnectionChange,
   });
 
   final Widget child;
 
   final bool requiresRunningApplication;
 
+  final Future<void> Function(String?) onDtdConnectionChange;
+
   @override
   State<SimulatedDevToolsWrapper> createState() =>
       SimulatedDevToolsWrapperState();
@@ -51,9 +57,10 @@
 
   late final ScrollController scrollController;
 
-  late ConnectedState connectionState;
+  bool get vmServiceConnected => vmServiceConnectionState.connected;
+  late ConnectedState vmServiceConnectionState;
 
-  bool get connected => connectionState.connected;
+  bool dtdConnected = false;
 
   @override
   void initState() {
@@ -62,10 +69,17 @@
 
     scrollController = ScrollController();
 
-    connectionState = serviceManager.connectedState.value;
+    vmServiceConnectionState = serviceManager.connectedState.value;
     addAutoDisposeListener(serviceManager.connectedState, () {
       setState(() {
-        connectionState = serviceManager.connectedState.value;
+        vmServiceConnectionState = serviceManager.connectedState.value;
+      });
+    });
+
+    dtdConnected = dtdManager.hasConnection;
+    addAutoDisposeListener(dtdManager.connection, () {
+      setState(() {
+        dtdConnected = dtdManager.hasConnection;
       });
     });
   }
@@ -84,7 +98,7 @@
       builder: (context, constraints) {
         final availableWidth = constraints.maxWidth;
         const environmentPanelMinWidth =
-            VmServiceConnection.totalControlsWidth + 2 * defaultSpacing;
+            VmServiceConnectionDisplay.totalControlsWidth + 2 * defaultSpacing;
 
         final environmentPanelFraction =
             environmentPanelMinWidth / availableWidth;
@@ -129,49 +143,25 @@
                                 style: theme.textTheme.titleMedium,
                               ),
                               const PaddedDivider(),
-                              VmServiceConnection(
-                                connected: connected,
+                              VmServiceConnectionDisplay(
+                                connected: vmServiceConnected,
                                 simController: simController,
                               ),
-                              const SizedBox(height: denseSpacing),
-                              // TODO(kenz): figure out how to simulate the DTD
-                              // connection. This may be non-trivial.
+                              const PaddedDivider(),
+                              DTDConnectionDisplay(
+                                simController: simController,
+                                connected: dtdConnected,
+                                onConnectionChange: widget.onDtdConnectionChange,
+                              ),
                               _SimulatedApi(
                                 simController: simController,
                                 requiresRunningApplication:
                                     widget.requiresRunningApplication,
-                                connectedToApplication: connected,
+                                connectedToApplication: vmServiceConnected,
                               ),
                               const PaddedDivider(),
                               Expanded(
-                                child: Column(
-                                  crossAxisAlignment: CrossAxisAlignment.start,
-                                  children: [
-                                    Row(
-                                      mainAxisAlignment:
-                                          MainAxisAlignment.spaceBetween,
-                                      children: [
-                                        Text(
-                                          'Logs:',
-                                          style: theme.textTheme.titleMedium,
-                                        ),
-                                        DevToolsButton.iconOnly(
-                                          icon: Icons.clear,
-                                          outlined: false,
-                                          tooltip: 'Clear logs',
-                                          onPressed: () =>
-                                              simController.messageLogs.clear(),
-                                        ),
-                                      ],
-                                    ),
-                                    const PaddedDivider.thin(),
-                                    Expanded(
-                                      child: _LogMessages(
-                                        simController: simController,
-                                      ),
-                                    ),
-                                  ],
-                                ),
+                                child: _LogsView(simController: simController),
                               ),
                             ],
                           ),
@@ -254,6 +244,42 @@
   }
 }
 
+class _LogsView extends StatelessWidget {
+  const _LogsView({required this.simController});
+
+  final SimulatedDevToolsController simController;
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Row(
+          mainAxisAlignment: MainAxisAlignment.spaceBetween,
+          children: [
+            Text(
+              'Logs:',
+              style: Theme.of(context).textTheme.titleMedium,
+            ),
+            DevToolsButton.iconOnly(
+              icon: Icons.clear,
+              outlined: false,
+              tooltip: 'Clear logs',
+              onPressed: () => simController.messageLogs.clear(),
+            ),
+          ],
+        ),
+        const PaddedDivider.thin(),
+        Expanded(
+          child: _LogMessages(
+            simController: simController,
+          ),
+        ),
+      ],
+    );
+  }
+}
+
 class _LogMessages extends StatefulWidget {
   const _LogMessages({required this.simController});
 
diff --git a/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/connection_ui/_connect.dart b/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/connection_ui/_connect.dart
new file mode 100644
index 0000000..5ab7cf4
--- /dev/null
+++ b/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/connection_ui/_connect.dart
@@ -0,0 +1,220 @@
+// Copyright 2023 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+part of '../_simulated_devtools_environment.dart';
+
+class _UriConnectionDisplay extends StatelessWidget {
+  const _UriConnectionDisplay({
+    required this.simController,
+    required this.connected,
+    required this.connectedLabel,
+    required this.disconnectedLabel,
+    required this.disconnectedHint,
+    required this.onConnect,
+    required this.onDisconnect,
+    required this.currentConnection,
+    this.help,
+  });
+
+  @visibleForTesting
+  static const totalControlsHeight = 45.0;
+
+  final SimulatedDevToolsController simController;
+  final bool connected;
+  final String connectedLabel;
+  final String disconnectedLabel;
+  final String disconnectedHint;
+  final void Function(String) onConnect;
+  final VoidCallback onDisconnect;
+  final String Function() currentConnection;
+  final Widget? help;
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: totalControlsHeight,
+      child: connected
+          ? _ConnectedDisplay(
+              simController: simController,
+              label: connectedLabel,
+              onDisconnect: onDisconnect,
+              currentConnection: currentConnection,
+              help: help,
+            )
+          : _DisconnectedDisplay(
+              simController: simController,
+              label: disconnectedLabel,
+              hint: disconnectedHint,
+              onConnect: onConnect,
+              help: help,
+            ),
+    );
+  }
+}
+
+class _ConnectedDisplay extends StatelessWidget {
+  const _ConnectedDisplay({
+    required this.simController,
+    required this.label,
+    required this.currentConnection,
+    required this.onDisconnect,
+    this.help,
+  });
+
+  final SimulatedDevToolsController simController;
+  final String label;
+  final String Function() currentConnection;
+  final VoidCallback onDisconnect;
+  final Widget? help;
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    return Row(
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Text(
+              label,
+              style: theme.regularTextStyle,
+            ),
+            Text(
+              currentConnection(),
+              style: theme.boldTextStyle,
+            ),
+          ],
+        ),
+        const Expanded(
+          child: SizedBox(width: denseSpacing),
+        ),
+        DevToolsButton(
+          elevated: true,
+          label: 'Disconnect',
+          onPressed: onDisconnect,
+        ),
+        if (help != null) ...[
+          const SizedBox(width: denseSpacing),
+          _ConnectionHelpButton(
+            dialogTitle: '$label help',
+            child: help!,
+          ),
+        ],
+      ],
+    );
+  }
+}
+
+class _DisconnectedDisplay extends StatefulWidget {
+  const _DisconnectedDisplay({
+    required this.simController,
+    required this.label,
+    required this.hint,
+    required this.onConnect,
+    this.help,
+  });
+
+  final SimulatedDevToolsController simController;
+  final String label;
+  final String hint;
+  final void Function(String) onConnect;
+  final Widget? help;
+
+  @override
+  State<_DisconnectedDisplay> createState() => _DisconnectedDisplayState();
+}
+
+class _DisconnectedDisplayState extends State<_DisconnectedDisplay> {
+  static const _connectFieldWidth = 300.0;
+
+  late final TextEditingController _connectTextFieldController;
+
+  @override
+  void initState() {
+    super.initState();
+    _connectTextFieldController = TextEditingController();
+  }
+
+  @override
+  void dispose() {
+    _connectTextFieldController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    return Row(
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        SizedBox(
+          width: _connectFieldWidth,
+          child: TextField(
+            autofocus: true,
+            style: theme.regularTextStyle,
+            decoration: InputDecoration(
+              contentPadding: const EdgeInsets.all(denseSpacing),
+              isDense: true,
+              border: const OutlineInputBorder(),
+              enabledBorder: OutlineInputBorder(
+                borderSide: BorderSide(width: 0.5, color: theme.focusColor),
+              ),
+              labelText: widget.label,
+              labelStyle: theme.regularTextStyle,
+              hintText: widget.hint,
+              hintStyle: theme.regularTextStyle,
+            ),
+            onSubmitted: widget.onConnect,
+            controller: _connectTextFieldController,
+          ),
+        ),
+        const SizedBox(width: denseSpacing),
+        DevToolsButton(
+          elevated: true,
+          label: 'Connect',
+          onPressed: () => widget.onConnect(_connectTextFieldController.text),
+        ),
+        if (widget.help != null) ...[
+          const SizedBox(width: denseSpacing),
+          _ConnectionHelpButton(
+            dialogTitle: '${widget.label} help',
+            child: widget.help!,
+          ),
+        ],
+      ],
+    );
+  }
+}
+
+class _ConnectionHelpButton extends StatelessWidget {
+  const _ConnectionHelpButton({
+    required this.dialogTitle,
+    required this.child,
+  });
+
+  final String dialogTitle;
+
+  final Widget child;
+
+  static const helpContentWidth = 550.0;
+
+  @override
+  Widget build(BuildContext context) {
+    return DevToolsButton.iconOnly(
+      icon: Icons.help_outline,
+      tooltip: 'Help',
+      onPressed: () {
+        showDevToolsDialog(
+          context: context,
+          title: dialogTitle,
+          content: SizedBox(
+            width: helpContentWidth,
+            child: child,
+          ),
+        );
+      },
+    );
+  }
+}
diff --git a/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/connection_ui/_dtd_connect.dart b/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/connection_ui/_dtd_connect.dart
new file mode 100644
index 0000000..9316bc6
--- /dev/null
+++ b/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/connection_ui/_dtd_connect.dart
@@ -0,0 +1,167 @@
+// Copyright 2024 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+part of '../_simulated_devtools_environment.dart';
+
+@visibleForTesting
+class DTDConnectionDisplay extends StatelessWidget {
+  const DTDConnectionDisplay({
+    super.key,
+    required this.simController,
+    required this.connected,
+    required this.onConnectionChange,
+  });
+
+  @visibleForTesting
+  static const totalControlsHeight = 45.0;
+  @visibleForTesting
+  static const totalControlsWidth = 415.0;
+
+  final SimulatedDevToolsController simController;
+  final bool connected;
+  final Future<void> Function(String?) onConnectionChange;
+
+  @override
+  Widget build(BuildContext context) {
+    return _UriConnectionDisplay(
+      simController: simController,
+      connected: connected,
+      connectedLabel: 'Dart Tooling Daemon connection: ',
+      disconnectedLabel: 'Dart Tooling Daemon Connection',
+      disconnectedHint: '(e.g., ws://127.0.0.1:57624)',
+      // TODO(kenz): this needs handling for bad input.
+      onConnect: (value) async {
+        value = value.trim();
+        if (value.startsWith('127.0.0.1')) {
+          value = 'ws://$value';
+        }
+        await onConnectionChange(value);
+        if (dtdManager.hasConnection) {
+          simController.logInfoEvent(
+            'Connected DTD to ${dtdManager.uri?.toString()}',
+          );
+        } else {
+          simController.logInfoEvent('Failed to connect DTD to $value');
+        }
+      },
+      onDisconnect: () async {
+        await onConnectionChange(null);
+        simController.logInfoEvent('Disconnected from DTD');
+      },
+      currentConnection: () => dtdManager.uri?.toString() ?? '--',
+      help: const DtdHelp(),
+    );
+  }
+}
+
+class DtdHelp extends StatelessWidget {
+  const DtdHelp({super.key});
+
+  static const dtdStartCommand = 'dart tooling-daemon --unrestricted';
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    return DefaultTextStyle(
+      style: theme.regularTextStyle,
+      child: Column(
+        children: [
+          RichText(
+            text: TextSpan(
+              style: theme.regularTextStyle,
+              text:
+                  'If your DevTools extension interacts with the Dart Tooling '
+                  'Daemon (DTD) through',
+              children: [
+                TextSpan(
+                  text: ' dtdManager ',
+                  style: theme.boldTextStyle,
+                ),
+                const TextSpan(
+                  text: 'then you will need to start a local instance of DTD '
+                      'to debug these features in the simulated environment. '
+                      'To start DTD locally:\n\n1. Run the following command '
+                      'from your terminal:',
+                ),
+              ],
+            ),
+          ),
+          Padding(
+            padding: const EdgeInsets.symmetric(vertical: defaultSpacing),
+            child: Row(
+              mainAxisSize: MainAxisSize.min,
+              children: [
+                const SelectableText(dtdStartCommand),
+                const SizedBox(width: defaultSpacing),
+                DevToolsButton.iconOnly(
+                  icon: Icons.content_copy,
+                  onPressed: () async {
+                    await Clipboard.setData(
+                      const ClipboardData(text: dtdStartCommand),
+                    );
+                  },
+                ),
+              ],
+            ),
+          ),
+          RichText(
+            text: TextSpan(
+              style: theme.regularTextStyle,
+              text: '2. This will output text to the command line:',
+              children: [
+                TextSpan(
+                  text: ' "The Dart Tooling Daemon is listening on '
+                      'ws://127.0.0.1:62630/". ',
+                  style: theme.boldTextStyle,
+                ),
+                const TextSpan(
+                  text: 'Copy the DTD URI and paste it into the "Dart '
+                      'Tooling Daemon Connection" text field to connect.',
+                ),
+              ],
+            ),
+          ),
+          const SizedBox(height: defaultSpacing),
+          Text(
+            'In a real environment, DTD will be started by the user\'s IDE or'
+            ' by DevTools, so your extension will inherit the existing DTD'
+            ' connection from DevTools.',
+            style: theme.subtleTextStyle,
+          ),
+          const SizedBox(height: defaultSpacing),
+          RichText(
+            text: TextSpan(
+              text: 'WARNING: if you are using DTD\'s file system APIs (',
+              style: theme.regularTextStyle.copyWith(
+                color: theme.colorScheme.tertiary,
+              ),
+              children: [
+                TextSpan(
+                  text: 'readFileAsString, writeFileAsString, listDirectories',
+                  style: theme.fixedFontStyle.copyWith(
+                    color: theme.colorScheme.tertiary,
+                  ),
+                ),
+                TextSpan(
+                  text:
+                      '), these will be more restricted in a real environment. '
+                      'In the simulated environment, your extension can access '
+                      'the entire file system; in a real environment, your '
+                      'extension will only have access to files within the '
+                      'user\'s project, which are determined by the workspace '
+                      'directores in the IDE, or, if DevTools was launched from '
+                      'the command line, then the project directories will be '
+                      'determined by the project root of the connected app.',
+                  style: theme.regularTextStyle.copyWith(
+                    color: theme.colorScheme.tertiary,
+                  ),
+                ),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/connection_ui/_vm_service_connect.dart b/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/connection_ui/_vm_service_connect.dart
new file mode 100644
index 0000000..c4720dc
--- /dev/null
+++ b/packages/devtools_extensions/lib/src/template/_simulated_devtools_environment/connection_ui/_vm_service_connect.dart
@@ -0,0 +1,83 @@
+// Copyright 2024 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+part of '../_simulated_devtools_environment.dart';
+
+@visibleForTesting
+class VmServiceConnectionDisplay extends StatelessWidget {
+  const VmServiceConnectionDisplay({
+    super.key,
+    required this.simController,
+    required this.connected,
+  });
+
+  @visibleForTesting
+  static const totalControlsHeight = 45.0;
+  @visibleForTesting
+  static const totalControlsWidth = 415.0;
+
+  final SimulatedDevToolsController simController;
+  final bool connected;
+
+  @override
+  Widget build(BuildContext context) {
+    return _UriConnectionDisplay(
+      simController: simController,
+      connected: connected,
+      connectedLabel: 'Debugging: ',
+      disconnectedLabel: 'Dart VM Service Connection',
+      disconnectedHint: '(e.g., http://127.0.0.1:60851/fH-kAEXc7MQ=/)',
+      onConnect: (value) => simController.updateVmServiceConnection(uri: value),
+      onDisconnect: () => simController.updateVmServiceConnection(uri: null),
+      currentConnection: () => serviceManager.service!.wsUri ?? '--',
+      help: const VmServiceHelp(),
+    );
+  }
+}
+
+class VmServiceHelp extends StatelessWidget {
+  const VmServiceHelp({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    return DefaultTextStyle(
+      style: theme.regularTextStyle,
+      child: Column(
+        children: [
+          Text(
+            'If your DevTools extension interacts with a running Dart '
+            'application, then you will need to run a test application and '
+            'connect it to the simulated environment.\n\n'
+            '1. Run a Dart or Flutter application.\n',
+            style: theme.regularTextStyle,
+          ),
+          RichText(
+            text: TextSpan(
+              style: theme.regularTextStyle,
+              text: '2. This will output text to the command line:',
+              children: [
+                TextSpan(
+                  text: ' "A Dart VM Service is available at: '
+                      'http://127.0.0.1:53985/6RVz1q0e9ok=". ',
+                  style: theme.boldTextStyle,
+                ),
+                const TextSpan(
+                  text: 'Copy the VM Service URI and paste it into the "Dart '
+                      'VM Service Connection" text field to connect.',
+                ),
+              ],
+            ),
+          ),
+          const SizedBox(height: defaultSpacing),
+          Text(
+            'In a real environment, your extension will inherit the existing '
+            'VM Service connection from DevTools.',
+            style: theme.subtleTextStyle,
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/packages/devtools_extensions/lib/src/template/devtools_extension.dart b/packages/devtools_extensions/lib/src/template/devtools_extension.dart
index e74afe8..6ab6762 100644
--- a/packages/devtools_extensions/lib/src/template/devtools_extension.dart
+++ b/packages/devtools_extensions/lib/src/template/devtools_extension.dart
@@ -197,6 +197,7 @@
         body: _useSimulatedEnvironment
             ? SimulatedDevToolsWrapper(
                 requiresRunningApplication: widget.requiresRunningApplication,
+                onDtdConnectionChange: extensionManager._connectToDtd,
                 child: child,
               )
             : child,