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 {