Hide extension button when embedded in an IDE (#9956)

* Hide the extension settings action when DevTools is running in an IDE.

Fixes https://github.com/flutter/devtools/issues/8507

* Ensure that the extensions action is displayed when showing an
extension on screen, even when embedded in an IDE.

* Replace https://flutter.dev/ with a data placeholder in
_controller_web.dart to prevent browser X-Frame-Options
iframe errors during local debugging.

* Add debugging devtools extensions skill

* add to release notes

* format

* Apply suggestions from code review

Co-authored-by: Kenzie Davisson <43759233+kenzieschmoll@users.noreply.github.com>

* Use OS-based browser opening commands

* Refactor HTML into a function

* remove embedMode information from skill, since it's unrelated

* Extract conditionals into local variable

* Use isEmbedded() to ensure that  embedMode=One or embedMode=Many are allowed

* format

---------

Co-authored-by: Kenzie Davisson <43759233+kenzieschmoll@users.noreply.github.com>
diff --git a/.agents/skills/debugging-devtools-extensions/SKILL.md b/.agents/skills/debugging-devtools-extensions/SKILL.md
new file mode 100644
index 0000000..5fbb2e4
--- /dev/null
+++ b/.agents/skills/debugging-devtools-extensions/SKILL.md
@@ -0,0 +1,63 @@
+---
+name: debugging-devtools-extensions
+description: Guidelines and step-by-step workflow for debugging DevTools extensions locally, including stub mode, fixed-port launching, browser auto-opening, URL query parameters, target app connection, and human-in-the-loop interaction. Use when debugging or testing DevTools extension behavior.
+---
+
+# Debugging DevTools Extensions
+
+Follow this workflow to test and debug DevTools extensions locally.
+
+## 1. Local Stub Extensions Mode (No Server Needed)
+
+When running DevTools in standalone web mode (`flutter run -d chrome`), DevTools does not run the `devtools_server` backend by default. To test extensions without a running server backend:
+
+1. Open [`packages/devtools_app/lib/src/shared/development_helpers.dart`](file:///Users/ryjohn/code/github/flutter/devtools/packages/devtools_app/lib/src/shared/development_helpers.dart#L57).
+2. Set `const _debugDevToolsExtensions = true;`.
+
+> [!WARNING]
+> Never commit `_debugDevToolsExtensions = true;` to git. A repository unit test (`development_helpers_test.dart`) enforces that this flag remains `false`.
+
+Activating stub mode registers the following mock extensions:
+- `foo_ext` (`package:foo`)
+- `bar_ext` (`package:bar`)
+- `provider_ext` (`package:provider`)
+
+## 2. Automated Launch & Browser Navigation
+
+The agent can automate running DevTools AND launching the browser directly to the target URL:
+
+### Step 2a: Launch DevTools on a Fixed Port
+In `packages/devtools_app`, launch DevTools specifying a fixed `--web-port`:
+```bash
+flutter run -d chrome --web-port=52941
+```
+
+### Step 2b: Open Browser to Target URL Automatically
+Use the system OS open command to launch Chrome/browser directly to the desired test URL:
+
+- **macOS**: `open "http://localhost:52941/foo_ext?embedMode=one"`
+- **Linux**: `xdg-open "http://localhost:52941/foo_ext?embedMode=one"`
+- **Windows**: `start "http://localhost:52941/foo_ext?embedMode=one"`
+
+## 3. Connecting to an End-User Target App
+
+To test against real pub package extensions:
+
+1. Run the sample app in `packages/devtools_extensions/example/app_that_uses_foo`:
+   ```bash
+   cd packages/devtools_extensions/example/app_that_uses_foo
+   flutter run -d chrome
+   ```
+2. Ask the user to copy/paste the VM Service URI from the terminal output (e.g. `ws://127.0.0.1:8181/xxx=/ws`).
+3. Open the browser automatically with the `uri` parameter using the appropriate OS command (as described in Step 2b):
+   - **macOS**: `open "http://localhost:52941/foo_ext?embedMode=one&uri=<VM_SERVICE_URI>"`
+   - **Linux**: `xdg-open "http://localhost:52941/foo_ext?embedMode=one&uri=<VM_SERVICE_URI>"`
+   - **Windows**: `start "http://localhost:52941/foo_ext?embedMode=one&uri=<VM_SERVICE_URI>"`
+
+## 4. Human Interaction & User Prompting Steps
+
+When an AI agent is performing this workflow:
+
+- **Obtaining VM Service URI**: When connecting to a target app, ask the user to provide the VM Service URI printed in the target app's console output (using `ask_question` or a direct prompt).
+- **Automated Browser Opening**: The agent should launch DevTools and execute the appropriate OS command (`open`, `xdg-open`, or `start` as described in Step 2b) to launch the browser automatically.
+- **Manual Visual Verification**: Ask the user to inspect the opened browser window and confirm whether the expected extension UI or behavior is visible.
diff --git a/packages/devtools_app/lib/src/extensions/embedded/_controller_web.dart b/packages/devtools_app/lib/src/extensions/embedded/_controller_web.dart
index 1da1d18..ab96f86 100644
--- a/packages/devtools_app/lib/src/extensions/embedded/_controller_web.dart
+++ b/packages/devtools_app/lib/src/extensions/embedded/_controller_web.dart
@@ -30,6 +30,35 @@
 /// [ui_web.PlatformViewRegistry], which [_viewIdIncrementer] is used to create.
 var _viewIdIncrementer = 0;
 
+/// HTML template for the placeholder view used when debugging extensions without
+/// a running DevTools server.
+String _debugExtensionPlaceholderHtml(String name) {
+  return '''
+<!DOCTYPE html>
+<html>
+<head>
+  <style>
+    body {
+      font-family: sans-serif;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      height: 100vh;
+      margin: 0;
+      background-color: #202124;
+      color: #e8eaed;
+    }
+  </style>
+</head>
+<body>
+  <h3>DevTools Extension Placeholder ($name)</h3>
+  <p>Local debugging placeholder view.</p>
+</body>
+</html>
+''';
+}
+
 class EmbeddedExtensionControllerImpl extends EmbeddedExtensionController
     with AutoDisposeControllerMixin {
   EmbeddedExtensionControllerImpl(super.extensionConfig);
@@ -42,7 +71,8 @@
 
   String get extensionUrl {
     if (debugDevToolsExtensions && !isDevToolsServerAvailable) {
-      return 'https://flutter.dev/';
+      final html = _debugExtensionPlaceholderHtml(extensionConfig.name);
+      return 'data:text/html;charset=utf-8,${Uri.encodeComponent(html)}';
     }
 
     final basePath = devtoolsAssetsBasePath(
diff --git a/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart b/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart
index 1f2073e..0b09230 100644
--- a/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart
+++ b/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart
@@ -9,6 +9,7 @@
 import 'package:provider/provider.dart';
 
 import '../../app.dart';
+import '../../extensions/extension_screen.dart';
 import '../../extensions/extension_settings.dart';
 import '../../screens/debugger/debugger_screen.dart';
 import '../../shared/analytics/prompt.dart';
@@ -57,14 +58,32 @@
          embedMode: embedMode,
        );
 
-  static List<Widget> defaultActions({Color? color}) => [
-    OpenSettingsAction(color: color),
-    if (FeatureFlags.devToolsExtensions.isEnabled &&
-        !DevToolsQueryParams.load().hideExtensions)
-      ExtensionSettingsAction(color: color),
-    ReportFeedbackButton(color: color),
-    OpenAboutAction(color: color),
-  ];
+  /// Returns the list of ScaffoldAction widgets.
+  static List<Widget> defaultActions({
+    Color? color,
+    Screen? currentScreen,
+    DevToolsQueryParams? queryParams,
+  }) {
+    queryParams ??= DevToolsQueryParams.load();
+    // Whether DevTools is embedded in an IDE displaying extension(s).
+    final isEmbeddedExtensionScreen =
+        isEmbedded() &&
+        (currentScreen is ExtensionScreen ||
+            queryParams.hideAllExceptExtensions);
+    // Show extension settings if extensions are enabled and not hidden, AND:
+    // - DevTools is running standalone in the browser (!isEmbedded), OR
+    // - DevTools is embedded in an IDE, but specifically showing an extension.
+    final showExtensionSettings =
+        FeatureFlags.devToolsExtensions.isEnabled &&
+        !queryParams.hideExtensions &&
+        (!isEmbedded() || isEmbeddedExtensionScreen);
+    return [
+      OpenSettingsAction(color: color),
+      if (showExtensionSettings) ExtensionSettingsAction(color: color),
+      ReportFeedbackButton(color: color),
+      OpenAboutAction(color: color),
+    ];
+  }
 
   /// The padding around the content in the DevTools UI.
   EdgeInsets get appPadding => EdgeInsets.fromLTRB(
diff --git a/packages/devtools_app/lib/src/framework/scaffold/status_line.dart b/packages/devtools_app/lib/src/framework/scaffold/status_line.dart
index 9c362da..114444d 100644
--- a/packages/devtools_app/lib/src/framework/scaffold/status_line.dart
+++ b/packages/devtools_app/lib/src/framework/scaffold/status_line.dart
@@ -126,7 +126,10 @@
         BulletSpacer(color: foregroundColor),
         Row(
           crossAxisAlignment: CrossAxisAlignment.end,
-          children: DevToolsScaffold.defaultActions(color: foregroundColor),
+          children: DevToolsScaffold.defaultActions(
+            color: foregroundColor,
+            currentScreen: currentScreen,
+          ),
         ),
       ],
     ];
diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
index ee62970..0d7aeb8 100644
--- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
+++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
@@ -77,7 +77,8 @@
 
 ## DevTools extension updates
 
-TODO: Remove this section if there are not any updates.
+* Hide the DevTools extensions menu button in single-screen embedded mode (`EmbedMode.embedOne`) on standard screens.
+  [#8507](https://github.com/flutter/devtools/issues/8507)
 
 ## Advanced developer mode updates
 
diff --git a/packages/devtools_app/test/framework/scaffold/scaffold_test.dart b/packages/devtools_app/test/framework/scaffold/scaffold_test.dart
index 74d806e..434ec2a 100644
--- a/packages/devtools_app/test/framework/scaffold/scaffold_test.dart
+++ b/packages/devtools_app/test/framework/scaffold/scaffold_test.dart
@@ -3,7 +3,10 @@
 // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
 
 import 'package:devtools_app/devtools_app.dart';
+import 'package:devtools_app/src/extensions/extension_screen.dart';
+import 'package:devtools_app/src/extensions/extension_settings.dart';
 import 'package:devtools_app/src/framework/scaffold/scaffold.dart';
+import 'package:devtools_app/src/shared/development_helpers.dart';
 import 'package:devtools_app/src/shared/framework/framework_controller.dart';
 import 'package:devtools_app/src/shared/managers/survey.dart';
 import 'package:devtools_app/src/shared/primitives/query_parameters.dart';
@@ -288,6 +291,69 @@
     );
     expect(scaffold.actions, isEmpty);
   });
+
+  test(
+    'defaultActions includes ExtensionSettingsAction based on EmbedMode and screen type',
+    () {
+      setGlobal(IdeTheme, IdeTheme());
+      expect(
+        DevToolsScaffold.defaultActions().any(
+          (w) => w is ExtensionSettingsAction,
+        ),
+        isTrue,
+      );
+      // In embedMany without extension query params or screen, it is hidden:
+      setGlobal(IdeTheme, IdeTheme(embedMode: EmbedMode.embedMany));
+      expect(
+        DevToolsScaffold.defaultActions().any(
+          (w) => w is ExtensionSettingsAction,
+        ),
+        isFalse,
+      );
+      // In embedMany with hideAllExceptExtensions, it is visible:
+      expect(
+        DevToolsScaffold.defaultActions(
+          queryParams: DevToolsQueryParams({'hide': 'all-except-extensions'}),
+        ).any((w) => w is ExtensionSettingsAction),
+        isTrue,
+      );
+      // In embedOne mode with standard screen, it is hidden:
+      setGlobal(IdeTheme, IdeTheme(embedMode: EmbedMode.embedOne));
+      expect(
+        DevToolsScaffold.defaultActions(
+          currentScreen: _screen1,
+        ).any((w) => w is ExtensionSettingsAction),
+        isFalse,
+      );
+      // In embedOne mode with ExtensionScreen, it is visible:
+      final extensionScreen = ExtensionScreen(
+        StubDevToolsExtensions.fooExtension,
+      );
+      expect(
+        DevToolsScaffold.defaultActions(
+          currentScreen: extensionScreen,
+        ).any((w) => w is ExtensionSettingsAction),
+        isTrue,
+      );
+    },
+  );
+
+  testWidgets(
+    'hides ExtensionSettingsAction in StatusLine for EmbedMode.embedOne',
+    (WidgetTester tester) async {
+      setGlobal(IdeTheme, IdeTheme(embedMode: EmbedMode.embedOne));
+      await tester.pumpWidget(
+        wrapScaffold(
+          DevToolsScaffold(
+            screens: const [_screen1],
+            page: _screen1.screenId,
+            embedMode: EmbedMode.embedOne,
+          ),
+        ),
+      );
+      expect(find.byType(ExtensionSettingsAction), findsNothing);
+    },
+  );
 }
 
 class _TestScreen extends Screen {