Skip to content

[dart_tooling_mcp_server] add get_widget_tree tool #63

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 14, 2025
Merged

Conversation

kenzieschmoll
Copy link
Contributor

This PR adds a tool that gets the widget tree for a running Flutter app. In its current form, this tool calls the ext.flutter.inspector.getRootWidgetTree service extensions with the following parameters:

'groupName': groupId,
'isSummaryTree': 'true',
'withPreviews': 'true',
'fullDetails': 'true',

This will return Widgets that were added to the tree from the user's code (it will not contain implementation widgets). @elliette what do you think of these default values?

Example response:

{description: [root], type: _ElementDiagnosticableTreeNode, style: dense, hasChildren: true, allowWrap: false, summaryTree: true, valueId: inspector-83, locationId: 0, creationLocation: {file: file:///Users/kenzieschmoll/develop/flutter/packages/flutter/lib/src/widgets/binding.dart, line: 1331, column: 24, name: RootWidget}, children: [{description: MyApp, type: _ElementDiagnosticableTreeNode, style: dense, hasChildren: true, allowWrap: false, summaryTree: true, valueId: inspector-84, locationId: 1, creationLocation: {file: file:///Users/kenzieschmoll/develop/dart-lang/ai/pkgs/dart_tooling_mcp_server/test_fixtures/counter_app/lib/main.dart, line: 8, column: 16, name: MyApp}, createdByLocalProject: true, children: [{description: MaterialApp, type: _ElementDiagnosticableTreeNode, style: dense, hasChildren: true, allowWrap: false, summaryTree: true, valueId: inspector-85, locationId: 2, creationLocation: {file: file:///Users/kenzieschmoll/develop/dart-lang/ai/pkgs/dart_tooling_mcp_server/test_fixtures/counter_app/lib/main.dart, line: 16, column: 12, name: MaterialApp}, createdByLocalProject: true, children: [{description: MyHomePage, type: _ElementDiagnosticableTreeNode, style: dense, hasChildren: true, allowWrap: false, summaryTree: true, valueId: inspector-86, locationId: 3, creationLocation: {file: file:///Users/kenzieschmoll/develop/dart-lang/ai/pkgs/dart_tooling_mcp_server/test_fixtures/counter_app/lib/main.dart, line: 21, column: 19, name: MyHomePage}, createdByLocalProject: true, children: [{description: Scaffold, type: _ElementDiagnosticableTreeNode, style: dense, hasChildren: true, allowWrap: false, summaryTree: true, valueId: inspector-87, locationId: 4, creationLocation: {file: file:///Users/kenzieschmoll/develop/dart-lang/ai/pkgs/dart_tooling_mcp_server/test_fixtures/counter_app/lib/main.dart, line: 46, column: 12, name: Scaffold}, createdByLocalProject: true, children: [{description: Center, type: _ElementDiagnosticableTreeNode, style: dense, hasChildren: true, allowWrap: false, summaryTree: true, valueId: inspector-88, locationId: 5, creationLocation: {file: file:///Users/kenzieschmoll/develop/dart-lang/ai/pkgs/dart_tooling_mcp_server/test_fixtures/counter_app/lib/main.dart, line: 51, column: 13, name: Center}, createdByLocalProject: true, children: [{description: Column, type: _ElementDiagnosticableTreeNode, style: dense, hasChildren: true, allowWrap: false, summaryTree: true, valueId: inspector-89, locationId: 6, creationLocation: {file: file:///Users/kenzieschmoll/develop/dart-lang/ai/pkgs/dart_tooling_mcp_server/test_fixtures/counter_app/lib/main.dart, line: 52, column: 16, name: Column}, createdByLocalProject: true, children: [{description: Row, type: _ElementDiagnosticableTreeNode, style: dense, hasChildren: true, allowWrap: false, summaryTree: true, valueId: inspector-47, locationId: 7, creationLocation: {file: file:///Users/kenzieschmoll/develop/dart-lang/ai/pkgs/dart_tooling_mcp_server/test_fixtures/counter_app/lib/main.dart, line: 55, column: 19, name: Row}, createdByLocalProject: true, children: [{description: Text, type: _ElementDiagnosticableTreeNode, style: dense, hasChildren: true, allowWrap: false, summaryTree: true, valueId: inspector-90, locationId: 8, creationLocation: {file: file:///Users/kenzieschmoll/develop/dart-lang/ai/pkgs/dart_tooling_mcp_server/test_fixtures/counter_app/lib/main.dart, line: 57, column: 17, name: Text}, createdByLocalProject: true, textPreview: You have pushed the button this many times:, children: [], widgetRuntimeType: Text, stateful: false}, {description: Text, type: _ElementDiagnosticableTreeNode, style: dense, hasChildren: true, allowWrap: false, summaryTree: true, valueId: inspector-91, locationId: 9, creationLocation: {file: file:///Users/kenzieschmoll/develop/dart-lang/ai/pkgs/dart_tooling_mcp_server/test_fixtures/counter_app/lib/main.dart, line: 58, column: 17, name: Text}, createdByLocalProject: true, textPreview: And this Row should create an overflow error because it contains way more text than could ever fit on a single Row on both a mobile app or the default size of a desktop app., children: [], widgetRuntimeType: Text, stateful: false}], widgetRuntimeType: Row, stateful: false}, {description: Text, type: _ElementDiagnosticableTreeNode, style: dense, hasChildren: true, allowWrap: false, summaryTree: true, valueId: inspector-92, locationId: 10, creationLocation: {file: file:///Users/kenzieschmoll/develop/dart-lang/ai/pkgs/dart_tooling_mcp_server/test_fixtures/counter_app/lib/main.dart, line: 65, column: 13, name: Text}, createdByLocalProject: true, textPreview: 0, children: [], widgetRuntimeType: Text, stateful: false}], widgetRuntimeType: Column, stateful: false}], widgetRuntimeType: Center, stateful: false}, {description: AppBar, type: _ElementDiagnosticableTreeNode, style: dense, hasChildren: true, allowWrap: false, summaryTree: true, valueId: inspector-93, locationId: 11, creationLocation: {file: file:///Users/kenzieschmoll/develop/dart-lang/ai/pkgs/dart_tooling_mcp_server/test_fixtures/counter_app/lib/main.dart, line: 47, column: 15, name: AppBar}, createdByLocalProject: true, children: [{description: Text, type: _ElementDiagnosticableTreeNode, style: dense, hasChildren: true, allowWrap: false, summaryTree: true, valueId: inspector-94, locationId: 12, creationLocation: {file: file:///Users/kenzieschmoll/develop/dart-lang/ai/pkgs/dart_tooling_mcp_server/test_fixtures/counter_app/lib/main.dart, line: 49, column: 16, name: Text}, createdByLocalProject: true, textPreview: Flutter Demo Home Page, children: [], widgetRuntimeType: Text, stateful: false}], widgetRuntimeType: AppBar, stateful: true}, {description: FloatingActionButton, type: _ElementDiagnosticableTreeNode, style: dense, hasChildren: true, allowWrap: false, summaryTree: true, valueId: inspector-95, locationId: 13, creationLocation: {file: file:///Users/kenzieschmoll/develop/dart-lang/ai/pkgs/dart_tooling_mcp_server/test_fixtures/counter_app/lib/main.dart, line: 72, column: 29, name: FloatingActionButton}, createdByLocalProject: true, children: [{description: Icon, type: _ElementDiagnosticableTreeNode, style: dense, hasChildren: true, allowWrap: false, summaryTree: true, valueId: inspector-96, locationId: 14, creationLocation: {file: file:///Users/kenzieschmoll/develop/dart-lang/ai/pkgs/dart_tooling_mcp_server/test_fixtures/counter_app/lib/main.dart, line: 75, column: 22, name: Icon}, createdByLocalProject: true, children: [], widgetRuntimeType: Icon, stateful: false}], widgetRuntimeType: FloatingActionButton, stateful: false}], widgetRuntimeType: Scaffold, stateful: true}], widgetRuntimeType: MyHomePage, stateful: true}], widgetRuntimeType: MaterialApp, stateful: true}], widgetRuntimeType: MyApp, stateful: false}], widgetRuntimeType: RootWidget, stateful: false}

See https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/service_extensions.dart/#L126 for other Widget Inspector service extensions we may be interested in exposing. It is possible that getDetailsSubtree could be a better candidate for a getting a smaller portion of the widget tree (for example getting the subtree of a widget referenced in a Flutter error).

Comment on lines +336 to +340
await vmService.callServiceExtension(
'$inspectorExtensionPrefix.disposeGroup',
isolateId: isolateId,
args: {'objectGroup': groupId},
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elliette is cleaning up this object group necessary? Or do you think we should re-use this object group for all uses of this tool and clean up when the server gets shut down? I'm not super familiar with why we use these object groups in the Inspector.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I'm also not familiar with the object group lifecycle. I see this comment: https://github.com/flutter/flutter/blob/296df33be866d0cd8387524e288d9b280e725795/packages/flutter/lib/src/widgets/widget_inspector.dart#L924-L927

But I'm not sure if how we should be creating / disposing object groups is documented anywhere. I need to do more investigation

Copy link

PR Health

Changelog Entry ✔️
Package Changed Files

Changes to files need to be accounted for in their respective changelogs.

@elliette
Copy link

Depending on how we expect this to be used, I'm not sure if fullDetails is necessary.

fullDetails returns a substantially larger tree: flutter/flutter#157309

All the information we need to display the tree in DevTools is available without fullDetails. We only set fullDetails to true when requesting an individual node in the tree.

@kenzieschmoll kenzieschmoll merged commit 74bc462 into main Apr 14, 2025
13 checks passed
@kenzieschmoll kenzieschmoll deleted the widget-tree branch April 14, 2025 21:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants