Show widget file path in inspector (#9972)

* Show widget file path in inspector

* Handle dispose and null ids

* update release note

* Move location to the tab bar

* show on all tabs

* fix analyzer

* Grab tab trailing directly

* Update goldens
diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart
index 171231e..bf772c4 100644
--- a/packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart
+++ b/packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart
@@ -31,6 +31,7 @@
 import '../../shared/diagnostics/diagnostics_node.dart';
 import '../../shared/diagnostics/inspector_service.dart';
 import '../../shared/diagnostics/primitives/instance_ref.dart';
+import '../../shared/diagnostics/primitives/source_location.dart';
 import '../../shared/framework/screen_controllers.dart';
 import '../../shared/globals.dart';
 import '../../shared/managers/notifications.dart';
@@ -55,6 +56,9 @@
 
   /// Layout properties for the widget.
   LayoutProperties? layoutProperties,
+
+  /// Source location where the selected widget was created.
+  InspectorSourceLocation? creationLocation,
 });
 
 /// This class is based on the InspectorPanel class from the Flutter IntelliJ
@@ -255,6 +259,7 @@
     widgetProperties: [],
     renderProperties: [],
     layoutProperties: null,
+    creationLocation: null,
   ));
 
   /// Whether the implementation widgets are hidden in the widget tree.
@@ -817,6 +822,7 @@
     final widgetProperties = <RemoteDiagnosticsNode>[];
     final renderProperties = <RemoteDiagnosticsNode>[];
     LayoutProperties? layoutProperties;
+    InspectorSourceLocation? creationLocation;
     final diagnostic = node?.diagnostic;
     final objectGroupApi = diagnostic?.objectGroupApi;
     if (diagnostic != null && objectGroupApi != null) {
@@ -824,7 +830,7 @@
         // Fetch widget properties:
         final wProperties = await diagnostic.getProperties(objectGroupApi);
         // Check if the selected node has changed, and if so return early:
-        if (_selectedNode.value != node) {
+        if (disposed || _selectedNode.value != node) {
           return;
         }
         widgetProperties.addAll(
@@ -838,11 +844,22 @@
           diagnostic,
           forFlexLayout: false,
         );
+        // Fetch creation location from the details subtree. Summary tree nodes
+        // omit creationLocation when loaded with fullDetails: false.
+        final detailsNode = await objectGroupApi.getDetailsSubtree(
+          diagnostic,
+          subtreeDepth: 0,
+        );
+        // Check if the selected node has changed, and if so return early:
+        if (disposed || _selectedNode.value != node) {
+          return;
+        }
+        creationLocation = detailsNode?.creationLocation;
         // Fetch RenderObject properties:
         for (final renderObject in renderProperties) {
           final rProperties = await renderObject.getProperties(objectGroupApi);
           // Check if the selected node has changed, and if so return early:
-          if (_selectedNode.value != node) {
+          if (disposed || _selectedNode.value != node) {
             return;
           }
           renderProperties.addAll(rProperties);
@@ -851,10 +868,12 @@
         _log.warning(e, st);
       }
     }
+    if (disposed) return;
     _selectedNodeProperties.value = (
       widgetProperties: widgetProperties,
       renderProperties: renderProperties,
       layoutProperties: layoutProperties,
+      creationLocation: creationLocation,
     );
   }
 
diff --git a/packages/devtools_app/lib/src/screens/inspector/widget_properties/properties_view.dart b/packages/devtools_app/lib/src/screens/inspector/widget_properties/properties_view.dart
index 34ec1ac..9a339db 100644
--- a/packages/devtools_app/lib/src/screens/inspector/widget_properties/properties_view.dart
+++ b/packages/devtools_app/lib/src/screens/inspector/widget_properties/properties_view.dart
@@ -2,14 +2,18 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
 
+import 'dart:async';
 import 'dart:math';
 
+import 'package:devtools_app_shared/service.dart';
 import 'package:devtools_app_shared/ui.dart';
+import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 
 import '../../../shared/analytics/constants.dart' as gac;
 import '../../../shared/console/widgets/description.dart';
 import '../../../shared/diagnostics/diagnostics_node.dart';
+import '../../../shared/globals.dart';
 import '../../../shared/primitives/utils.dart';
 import '../../../shared/ui/tab.dart';
 import '../inspector_controller.dart';
@@ -76,12 +80,14 @@
         final widgetProperties = properties.widgetProperties;
         final renderProperties = properties.renderProperties;
         final layoutProperties = properties.layoutProperties;
-
         final renderTabExists = renderProperties.isNotEmpty;
         final flexExplorerTabExists = selectedNode?.isFlexLayout ?? false;
 
         return AnalyticsTabbedView(
           gaScreen: gac.inspector,
+          trailingWidgets: [
+            WidgetCreationLocationTrailing(controller: widget.controller),
+          ],
           onTabChanged: (int tabIndex) {
             _lastSelectedTab = _getTabForIndex(
               tabIndex,
@@ -160,6 +166,90 @@
   ];
 }
 
+/// Displays the source file path for the selected widget in the tab bar.
+///
+/// Matches the legacy inspector format: `filename.dart:line:column`.
+/// Tapping the link navigates the connected IDE to the source location.
+class WidgetCreationLocationTrailing extends StatefulWidget {
+  const WidgetCreationLocationTrailing({super.key, required this.controller});
+
+  final InspectorController controller;
+
+  @override
+  State<WidgetCreationLocationTrailing> createState() =>
+      _WidgetCreationLocationTrailingState();
+}
+
+class _WidgetCreationLocationTrailingState
+    extends State<WidgetCreationLocationTrailing> {
+  late final TapGestureRecognizer _tapRecognizer;
+
+  @override
+  void initState() {
+    super.initState();
+    _tapRecognizer = TapGestureRecognizer()
+      ..onTap = () {
+        unawaited(_navigateToLocation());
+      };
+  }
+
+  @override
+  void dispose() {
+    _tapRecognizer.dispose();
+    super.dispose();
+  }
+
+  Future<void> _navigateToLocation() async {
+    final location =
+        widget.controller.selectedNodeProperties.value.creationLocation;
+    final file = location?.getFile();
+    if (file == null) {
+      return;
+    }
+
+    await serviceConnection.serviceManager.service?.navigateToCode(
+      fileUriString: file,
+      line: location!.getLine(),
+      column: location.getColumn(),
+      source: 'devtools.inspector',
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return ValueListenableBuilder<WidgetTreeNodeProperties>(
+      valueListenable: widget.controller.selectedNodeProperties,
+      builder: (context, properties, _) {
+        final location = properties.creationLocation;
+        final file = location?.getFile();
+        if (file == null) {
+          return const SizedBox.shrink();
+        }
+
+        final line = location!.getLine();
+        final column = location.getColumn();
+        final shortLocation = '${fileNameFromUri(file)}:$line:$column';
+        final fullLocation = '$file:$line:$column';
+
+        return Padding(
+          padding: const EdgeInsets.symmetric(horizontal: denseSpacing),
+          child: DevToolsTooltip(
+            message: fullLocation,
+            child: RichText(
+              overflow: TextOverflow.ellipsis,
+              text: TextSpan(
+                text: shortLocation,
+                style: Theme.of(context).linkTextStyle,
+                recognizer: _tapRecognizer,
+              ),
+            ),
+          ),
+        );
+      },
+    );
+  }
+}
+
 /// Displays a widget's properties, including the layout properties and a
 /// layout visualizer.
 class PropertiesView extends StatefulWidget {
diff --git a/packages/devtools_app/lib/src/shared/diagnostics/inspector_service.dart b/packages/devtools_app/lib/src/shared/diagnostics/inspector_service.dart
index e983755..6081c5d 100644
--- a/packages/devtools_app/lib/src/shared/diagnostics/inspector_service.dart
+++ b/packages/devtools_app/lib/src/shared/diagnostics/inspector_service.dart
@@ -922,6 +922,24 @@
   }
 
   @override
+  Future<RemoteDiagnosticsNode?> getDetailsSubtree(
+    RemoteDiagnosticsNode? node, {
+    int subtreeDepth = 2,
+  }) async {
+    if (node == null || node.valueRef.id == null) return null;
+    return parseDiagnosticsNodeDaemon(
+      invokeServiceMethodDaemonParams(
+        WidgetInspectorServiceExtensions.getDetailsSubtree.name,
+        {
+          'objectGroup': groupName,
+          'arg': node.valueRef.id,
+          'subtreeDepth': subtreeDepth.toString(),
+        },
+      ),
+    );
+  }
+
+  @override
   bool isLocalClass(RemoteDiagnosticsNode node) =>
       inspectorService.isLocalClass(node);
 }
diff --git a/packages/devtools_app/lib/src/shared/diagnostics/object_group_api.dart b/packages/devtools_app/lib/src/shared/diagnostics/object_group_api.dart
index b1195a1..328d50b 100644
--- a/packages/devtools_app/lib/src/shared/diagnostics/object_group_api.dart
+++ b/packages/devtools_app/lib/src/shared/diagnostics/object_group_api.dart
@@ -39,4 +39,10 @@
   );
 
   Future<List<T>> getProperties(InspectorInstanceRef instanceRef);
+
+  /// Returns a details subtree for [node], including creation location data.
+  ///
+  /// Pass a small [subtreeDepth] (for example `0`) when only node-level details
+  /// such as creation location are needed.
+  Future<T?> getDetailsSubtree(T? node, {int subtreeDepth = 2});
 }
diff --git a/packages/devtools_app/lib/src/shared/ui/tab.dart b/packages/devtools_app/lib/src/shared/ui/tab.dart
index 6961b55..07cb85a 100644
--- a/packages/devtools_app/lib/src/shared/ui/tab.dart
+++ b/packages/devtools_app/lib/src/shared/ui/tab.dart
@@ -73,7 +73,7 @@
 /// value. This ensures that data being refreshed, or widget tree rebuilds don't
 /// send spurious analytics events.
 class AnalyticsTabbedView extends StatefulWidget {
-  AnalyticsTabbedView({
+  const AnalyticsTabbedView({
     super.key,
     required this.tabs,
     required this.gaScreen,
@@ -82,15 +82,18 @@
     this.initialSelectedIndex,
     this.analyticsSessionIdentifier,
     this.staticSingleTab = false,
-  }) : trailingWidgets = List.generate(
-         tabs.length,
-         (index) => tabs[index].tab.trailing ?? const SizedBox(),
-       );
+    this.trailingWidgets = const [],
+  });
 
   final List<TabAndView> tabs;
 
   final String gaScreen;
 
+  /// Shared trailing widgets shown on the right side of the tab bar for every
+  /// tab.
+  ///
+  /// Per-tab trailings from `DevToolsTab.trailing` are shown separately for the
+  /// currently selected tab.
   final List<Widget> trailingWidgets;
 
   final int? initialSelectedIndex;
@@ -212,7 +215,13 @@
               tabController: _tabController,
               staticSingleTab: widget.staticSingleTab,
             ),
-            widget.trailingWidgets[_currentTabControllerIndex],
+            Row(
+              mainAxisSize: MainAxisSize.min,
+              children: [
+                ?widget.tabs[_currentTabControllerIndex].tab.trailing,
+                ...widget.trailingWidgets,
+              ],
+            ),
           ],
         ),
       ),
diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
index 01ad0a7..b8955e4 100644
--- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
+++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
@@ -21,7 +21,10 @@
 
 ## Inspector updates
 
-TODO: Remove this section if there are not any updates.
+* Added the widget source file path to the Inspector details pane
+  (`filename.dart:line:column`), matching legacy Inspector behavior. -
+  [#9972](https://github.com/flutter/devtools/pull/9972),
+  [#9922](https://github.com/flutter/devtools/issues/9922)
 
 ## Performance updates
 
diff --git a/packages/devtools_app/test/screens/inspector/inspector_integration_test.dart b/packages/devtools_app/test/screens/inspector/inspector_integration_test.dart
index 47eb492..3f09b29 100644
--- a/packages/devtools_app/test/screens/inspector/inspector_integration_test.dart
+++ b/packages/devtools_app/test/screens/inspector/inspector_integration_test.dart
@@ -708,22 +708,3 @@
     await inspectorService.addPubRootDirectories([rootLibrary]);
   }
 }
-
-extension _ObjectGroupTestExtension on ObjectGroup {
-  Future<RemoteDiagnosticsNode?> getDetailsSubtree(
-    RemoteDiagnosticsNode? node, {
-    int subtreeDepth = 2,
-  }) async {
-    if (node == null) return null;
-    final args = {
-      'objectGroup': groupName,
-      'arg': node.valueRef.id,
-      'subtreeDepth': subtreeDepth.toString(),
-    };
-    final json = await invokeServiceMethodDaemonParams(
-      WidgetInspectorServiceExtensions.getDetailsSubtree.name,
-      args,
-    );
-    return parseDiagnosticsNodeHelper(json as Map<String, Object?>?);
-  }
-}
diff --git a/packages/devtools_app/test/screens/inspector/widget_creation_location_trailing_test.dart b/packages/devtools_app/test/screens/inspector/widget_creation_location_trailing_test.dart
new file mode 100644
index 0000000..7a93514
--- /dev/null
+++ b/packages/devtools_app/test/screens/inspector/widget_creation_location_trailing_test.dart
@@ -0,0 +1,122 @@
+// Copyright 2026 The Flutter Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
+
+import 'package:devtools_app/src/screens/inspector/inspector_controller.dart';
+import 'package:devtools_app/src/screens/inspector/widget_properties/properties_view.dart';
+import 'package:devtools_app/src/shared/diagnostics/primitives/source_location.dart';
+import 'package:devtools_app_shared/ui.dart';
+import 'package:devtools_app_shared/utils.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart' hide Fake;
+import 'package:mockito/mockito.dart';
+
+void main() {
+  setUp(() {
+    setGlobal(IdeTheme, IdeTheme());
+  });
+
+  Widget wrapTrailing(Widget child) {
+    return MaterialApp(
+      theme: themeFor(
+        isDarkTheme: false,
+        ideTheme: IdeTheme(),
+        theme: ThemeData(useMaterial3: true, colorScheme: lightColorScheme),
+      ),
+      home: Scaffold(body: child),
+    );
+  }
+
+  testWidgets('shows file name with line and column', (
+    WidgetTester tester,
+  ) async {
+    final controller = _TestInspectorController();
+    addTearDown(controller.dispose);
+
+    final location = InspectorSourceLocation({
+      'file': 'file:///Users/prismo/flutter_app/lib/main.dart',
+      'line': 109,
+      'column': 23,
+    }, null);
+    controller.setProperties((
+      widgetProperties: const [],
+      renderProperties: const [],
+      layoutProperties: null,
+      creationLocation: location,
+    ));
+
+    await tester.pumpWidget(
+      wrapTrailing(WidgetCreationLocationTrailing(controller: controller)),
+    );
+
+    expect(find.byType(RichText), findsOneWidget);
+    final richText = tester.widget<RichText>(find.byType(RichText));
+    expect(richText.text.toPlainText(), 'main.dart:109:23');
+  });
+
+  testWidgets('tooltip includes the full file URI', (
+    WidgetTester tester,
+  ) async {
+    final controller = _TestInspectorController();
+    addTearDown(controller.dispose);
+
+    final location = InspectorSourceLocation({
+      'file': 'file:///Users/prismo/flutter_app/lib/main.dart',
+      'line': 109,
+      'column': 23,
+    }, null);
+    controller.setProperties((
+      widgetProperties: const [],
+      renderProperties: const [],
+      layoutProperties: null,
+      creationLocation: location,
+    ));
+
+    await tester.pumpWidget(
+      wrapTrailing(WidgetCreationLocationTrailing(controller: controller)),
+    );
+
+    final tooltip =
+        tester.widget(find.byType(DevToolsTooltip)) as DevToolsTooltip;
+    expect(
+      tooltip.message,
+      'file:///Users/prismo/flutter_app/lib/main.dart:109:23',
+    );
+  });
+
+  testWidgets('hides when creation location is unavailable', (
+    WidgetTester tester,
+  ) async {
+    final controller = _TestInspectorController();
+    addTearDown(controller.dispose);
+
+    await tester.pumpWidget(
+      wrapTrailing(WidgetCreationLocationTrailing(controller: controller)),
+    );
+
+    expect(find.byType(RichText), findsNothing);
+  });
+}
+
+class _TestInspectorController extends Fake implements InspectorController {
+  final _selectedNodeProperties = ValueNotifier<WidgetTreeNodeProperties>((
+    widgetProperties: const [],
+    renderProperties: const [],
+    layoutProperties: null,
+    creationLocation: null,
+  ));
+
+  @override
+  ValueListenable<WidgetTreeNodeProperties> get selectedNodeProperties =>
+      _selectedNodeProperties;
+
+  void setProperties(WidgetTreeNodeProperties properties) {
+    _selectedNodeProperties.value = properties;
+  }
+
+  @override
+  void dispose() {
+    _selectedNodeProperties.dispose();
+  }
+}
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_2_error_selected.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_2_error_selected.png
index 31bbdf6..284f6c0 100644
--- a/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_2_error_selected.png
+++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_2_error_selected.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_collapsed.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_collapsed.png
index 8dd6dad..4544741 100644
--- a/packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_collapsed.png
+++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_collapsed.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_hidden.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_hidden.png
index 8431c62..4c6d3d0 100644
--- a/packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_hidden.png
+++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_hidden.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center.png
index b02d1e0..211c291 100644
--- a/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center.png
+++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center.png
Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/memory/load_offline_data_profile_tab.png b/packages/devtools_app/test/test_infra/goldens/memory/load_offline_data_profile_tab.png
index 112c1d0..390d7a4 100644
--- a/packages/devtools_app/test/test_infra/goldens/memory/load_offline_data_profile_tab.png
+++ b/packages/devtools_app/test/test_infra/goldens/memory/load_offline_data_profile_tab.png
Binary files differ
diff --git a/packages/devtools_test/lib/src/mocks/mocks.dart b/packages/devtools_test/lib/src/mocks/mocks.dart
index ca204b3..c803c75 100644
--- a/packages/devtools_test/lib/src/mocks/mocks.dart
+++ b/packages/devtools_test/lib/src/mocks/mocks.dart
@@ -81,6 +81,7 @@
         widgetProperties: [],
         renderProperties: [],
         layoutProperties: null,
+        creationLocation: null,
       ));
 
   @override