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 {