diff --git a/.agents/skills/debugging-devtools-extensions/SKILL.md b/.agents/skills/debugging-devtools-extensions/SKILL.md new file mode 100644 index 00000000000..5fbb2e4e881 --- /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="` + - **Linux**: `xdg-open "http://localhost:52941/foo_ext?embedMode=one&uri="` + - **Windows**: `start "http://localhost:52941/foo_ext?embedMode=one&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 1da1d1846ae..ab96f867ec0 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 @@ import 'controller.dart'; /// [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 ''' + + + + + + +

DevTools Extension Placeholder ($name)

+

Local debugging placeholder view.

+ + +'''; +} + class EmbeddedExtensionControllerImpl extends EmbeddedExtensionController with AutoDisposeControllerMixin { EmbeddedExtensionControllerImpl(super.extensionConfig); @@ -42,7 +71,8 @@ class EmbeddedExtensionControllerImpl extends EmbeddedExtensionController 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 1f2073e384a..13e92675130 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:flutter/material.dart'; 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,26 @@ class DevToolsScaffold extends StatefulWidget { embedMode: embedMode, ); - static List 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 defaultActions({Color? color, Screen? currentScreen}) { + final queryParams = DevToolsQueryParams.load(); + + // If DevTools is running in an IDE (EmbedMode.embedOne), then hide + // [ExtensionSettingsAction], unless this screen is showing an extension. + final showExtensionSettings = + FeatureFlags.devToolsExtensions.isEnabled && + !queryParams.hideExtensions && + (ideTheme.embedMode != EmbedMode.embedOne || + currentScreen is ExtensionScreen || + queryParams.hideAllExceptExtensions); + + 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 54e78dc2954..c638f7fde8b 100644 --- a/packages/devtools_app/lib/src/framework/scaffold/status_line.dart +++ b/packages/devtools_app/lib/src/framework/scaffold/status_line.dart @@ -124,7 +124,10 @@ class StatusLine extends StatelessWidget { 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 2d797c8109e..f734451ed1a 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -69,7 +69,8 @@ TODO: Remove this section if there are not any updates. ## 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 74d806eca1f..50749322124 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,64 @@ void main() { ); 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, + ); + + setGlobal(IdeTheme, IdeTheme(embedMode: EmbedMode.embedMany)); + expect( + DevToolsScaffold.defaultActions().any( + (w) => w is ExtensionSettingsAction, + ), + isTrue, + ); + + setGlobal(IdeTheme, IdeTheme(embedMode: EmbedMode.embedOne)); + // Standard screen in embedOne mode hides ExtensionSettingsAction + expect( + DevToolsScaffold.defaultActions( + currentScreen: _screen1, + ).any((w) => w is ExtensionSettingsAction), + isFalse, + ); + + // ExtensionScreen in embedOne mode shows ExtensionSettingsAction + 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 {