Remove AI assistant experiment and panel (#9797)
diff --git a/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart b/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart
index 8c34b71..1f2073e 100644
--- a/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart
+++ b/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart
@@ -11,7 +11,6 @@
import '../../app.dart';
import '../../extensions/extension_settings.dart';
import '../../screens/debugger/debugger_screen.dart';
-import '../../shared/ai_assistant/widgets/ai_assistant_pane.dart';
import '../../shared/analytics/prompt.dart';
import '../../shared/config_specific/drag_and_drop/drag_and_drop.dart';
import '../../shared/config_specific/import_export/import_export.dart';
@@ -317,11 +316,6 @@
!offlineDataController.showingOfflineData.value;
final showConsole =
isConnectedAppView && _currentScreen.showConsole(widget.embedMode);
- final showAiAssistant =
- FeatureFlags.aiAssistant.isEnabled &&
- isConnectedAppView &&
- _currentScreen.showAiAssistant();
- final showBottomPane = showConsole || showAiAssistant;
final containsSingleSimpleScreen =
widget.screens.length == 1 && widget.screens.first is SimpleScreen;
final showAppBar =
@@ -353,7 +347,7 @@
body: OutlineDecoration.onlyTop(
child: Padding(
padding: widget.appPadding,
- child: showBottomPane
+ child: showConsole
? SplitPane(
axis: Axis.vertical,
initialFractions: const [0.8, 0.2],
@@ -367,10 +361,7 @@
content,
BottomPane(
screenId: _currentScreen.screenId,
- tabs: [
- if (showConsole) const ConsolePane(),
- if (showAiAssistant) const AiAssistantPane(),
- ],
+ tabs: const [ConsolePane()],
),
],
)
diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen.dart b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen.dart
index 1ac60f6..6b8a117 100644
--- a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen.dart
+++ b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen.dart
@@ -23,9 +23,6 @@
bool showConsole(EmbedMode embedMode) => !embedMode.embedded;
@override
- bool showAiAssistant() => true;
-
- @override
String get docPageId => screenId;
@override
diff --git a/packages/devtools_app/lib/src/screens/network/network_screen.dart b/packages/devtools_app/lib/src/screens/network/network_screen.dart
index a9358f3..66843cf 100644
--- a/packages/devtools_app/lib/src/screens/network/network_screen.dart
+++ b/packages/devtools_app/lib/src/screens/network/network_screen.dart
@@ -40,9 +40,6 @@
String get docPageId => screenId;
@override
- bool showAiAssistant() => true;
-
- @override
Widget buildScreenBody(BuildContext context) => const NetworkScreenBody();
@override
diff --git a/packages/devtools_app/lib/src/shared/ai_assistant/ai_controller.dart b/packages/devtools_app/lib/src/shared/ai_assistant/ai_controller.dart
deleted file mode 100644
index 879e9b2..0000000
--- a/packages/devtools_app/lib/src/shared/ai_assistant/ai_controller.dart
+++ /dev/null
@@ -1,26 +0,0 @@
-// 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_shared/utils.dart';
-
-import 'ai_message_types.dart';
-
-class AiController extends DisposableController
- with AutoDisposeControllerMixin {
- AiController();
-
- Future<ChatMessage> sendMessage(ChatMessage _) async {
- await Future.delayed(const Duration(seconds: 3));
- return const ChatMessage(text: _loremIpsum, isUser: false);
- }
-}
-
-const _loremIpsum = '''
-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
-incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
-nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
-Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
-fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
-culpa qui officia deserunt mollit anim id est laborum.
-''';
diff --git a/packages/devtools_app/lib/src/shared/ai_assistant/ai_message_types.dart b/packages/devtools_app/lib/src/shared/ai_assistant/ai_message_types.dart
deleted file mode 100644
index c712236..0000000
--- a/packages/devtools_app/lib/src/shared/ai_assistant/ai_message_types.dart
+++ /dev/null
@@ -1,9 +0,0 @@
-// 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.
-
-class ChatMessage {
- const ChatMessage({required this.text, required this.isUser});
- final String text;
- final bool isUser;
-}
diff --git a/packages/devtools_app/lib/src/shared/ai_assistant/widgets/ai_assistant_pane.dart b/packages/devtools_app/lib/src/shared/ai_assistant/widgets/ai_assistant_pane.dart
deleted file mode 100644
index d8c8c5f..0000000
--- a/packages/devtools_app/lib/src/shared/ai_assistant/widgets/ai_assistant_pane.dart
+++ /dev/null
@@ -1,226 +0,0 @@
-// Copyright 2025 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 'dart:math' as math;
-
-import 'package:devtools_app_shared/ui.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-
-import '../../../framework/scaffold/bottom_pane.dart';
-import '../../ui/tab.dart';
-import '../../utils/utils.dart';
-import '../ai_controller.dart';
-import '../ai_message_types.dart';
-
-class AiAssistantPane extends StatefulWidget implements TabbedPane {
- const AiAssistantPane({super.key});
-
- @override
- DevToolsTab get tab => DevToolsTab.create(
- tabName: AiAssistantPane._tabName,
- gaPrefix: AiAssistantPane._gaPrefix,
- );
-
- static const _tabName = 'AI Assistant';
- static const _gaPrefix = 'aiAssistant';
-
- @override
- State<AiAssistantPane> createState() => _AiAssistantPaneState();
-}
-
-class _AiAssistantPaneState extends State<AiAssistantPane> {
- static const _baseOverscrollPadding = 125.0;
- static const _spinnerHeight = 50.0;
- static const _scrollDuration = Duration(milliseconds: 250);
-
- final _textController = TextEditingController();
- final _messages = <ChatMessage>[];
- final _scrollController = ScrollController();
- final _aiController = AiController();
- late final FocusNode _focusNode;
-
- bool _isThinking = false;
- double _overscrollPadding = _baseOverscrollPadding;
-
- @override
- void initState() {
- super.initState();
- _focusNode = FocusNode(onKeyEvent: _handleEnterKey);
- }
-
- @override
- void dispose() {
- _focusNode.dispose();
- _textController.dispose();
- super.dispose();
- }
-
- KeyEventResult _handleEnterKey(FocusNode node, KeyEvent event) {
- final isEnterKey =
- event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.enter;
-
- if (isEnterKey && !HardwareKeyboard.instance.isShiftPressed) {
- if (!_isThinking) {
- safeUnawaited(_sendMessage());
- }
- return KeyEventResult.handled;
- }
-
- return KeyEventResult.ignored;
- }
-
- Future<void> _sendMessage() async {
- final messageText = _textController.text;
- if (messageText.isEmpty) return;
- _textController.clear();
-
- final userMessage = ChatMessage(text: messageText, isUser: true);
- setState(() {
- _overscrollPadding = _calculateOverscrollPadding(userMessage);
- _isThinking = true;
- _messages.add(userMessage);
- });
- _scrollToBottom();
-
- final aiResponse = await _aiController.sendMessage(userMessage);
- setState(() {
- _isThinking = false;
- _overscrollPadding = _calculateOverscrollPadding(aiResponse);
- _messages.add(aiResponse);
- });
- _scrollToBottom();
- }
-
- void _scrollToBottom() {
- WidgetsBinding.instance.addPostFrameCallback((_) {
- if (_scrollController.hasClients) {
- safeUnawaited(
- _scrollController.animateTo(
- _scrollController.position.maxScrollExtent,
- duration: _scrollDuration,
- curve: Curves.ease,
- ),
- );
- }
- });
- }
-
- double _calculateOverscrollPadding(ChatMessage message) {
- final messageHeight =
- message.text.split('\n').length * (defaultFontSize + densePadding);
- final overscrollPadding = _baseOverscrollPadding + messageHeight;
- return message.isUser
- ? overscrollPadding + _spinnerHeight
- : overscrollPadding;
- }
-
- @override
- Widget build(BuildContext context) {
- return LayoutBuilder(
- builder: (context, constraints) {
- return Column(
- children: [
- Expanded(
- child: ListView.builder(
- padding: EdgeInsets.only(
- bottom: math.max(
- 0,
- constraints.maxHeight - _overscrollPadding,
- ),
- ),
- controller: _scrollController,
- itemCount: _isThinking
- ? _messages.length + 1
- : _messages.length,
- itemBuilder: (context, index) {
- if (_isThinking && index == _messages.length) {
- return const _ThinkingSpinner();
- }
- return _ChatMessageBubble(message: _messages[index]);
- },
- ),
- ),
- ConstrainedBox(
- constraints: BoxConstraints(maxHeight: constraints.maxHeight),
- child: Padding(
- padding: const EdgeInsets.all(denseSpacing),
- child: RoundedOutlinedBorder(
- child: Padding(
- // ignore: prefer-correct-edge-insets-constructor, false positive.
- padding: const EdgeInsets.fromLTRB(
- defaultSpacing,
- noPadding,
- defaultSpacing,
- densePadding,
- ),
- child: TextField(
- controller: _textController,
- focusNode: _focusNode,
- keyboardType: TextInputType.multiline,
- textAlignVertical: TextAlignVertical.center,
- minLines: 1,
- maxLines: 10,
- decoration: InputDecoration(
- hintText: 'Ask a question...',
- border: InputBorder.none,
- suffixIcon: IconButton(
- icon: const Icon(Icons.send),
- onPressed: _isThinking ? null : _sendMessage,
- ),
- ),
- ),
- ),
- ),
- ),
- ),
- ],
- );
- },
- );
- }
-}
-
-class _ChatMessageBubble extends StatelessWidget {
- const _ChatMessageBubble({required this.message});
-
- final ChatMessage message;
-
- @override
- Widget build(BuildContext context) {
- final colorScheme = Theme.of(context).colorScheme;
- return Align(
- alignment: message.isUser ? Alignment.centerRight : Alignment.centerLeft,
- child: Container(
- decoration: BoxDecoration(
- color: message.isUser
- ? colorScheme.primaryContainer
- : colorScheme.secondaryContainer,
- borderRadius: defaultBorderRadius,
- ),
- padding: const EdgeInsets.all(defaultSpacing),
- margin: const EdgeInsets.all(denseSpacing),
- child: Text(message.text),
- ),
- );
- }
-}
-
-class _ThinkingSpinner extends StatelessWidget {
- const _ThinkingSpinner();
-
- @override
- Widget build(BuildContext context) {
- return const Align(
- alignment: Alignment.centerLeft,
- child: Padding(
- padding: EdgeInsets.symmetric(
- vertical: denseSpacing,
- horizontal: extraLargeSpacing,
- ),
- child: CircularProgressIndicator(),
- ),
- );
- }
-}
diff --git a/packages/devtools_app/lib/src/shared/feature_flags.dart b/packages/devtools_app/lib/src/shared/feature_flags.dart
index c30e6be..a5cef0c 100644
--- a/packages/devtools_app/lib/src/shared/feature_flags.dart
+++ b/packages/devtools_app/lib/src/shared/feature_flags.dart
@@ -77,14 +77,6 @@
enabled: true,
);
- /// Flag to enable the AI Assistant.
- ///
- /// https://github.com/flutter/devtools/issues/9590
- static final aiAssistant = BooleanFeatureFlag(
- name: 'aiAssistant',
- enabled: enableExperiments,
- );
-
/// A set of all the boolean feature flags for debugging purposes.
///
/// When adding a new boolean flag, you are responsible for adding it to this
@@ -94,7 +86,6 @@
devToolsExtensions,
dapDebugging,
inspectorV2,
- aiAssistant,
};
/// A set of all the Flutter channel feature flags for debugging purposes.
diff --git a/packages/devtools_app/lib/src/shared/framework/screen.dart b/packages/devtools_app/lib/src/shared/framework/screen.dart
index 0cdc840..c3b4931 100644
--- a/packages/devtools_app/lib/src/shared/framework/screen.dart
+++ b/packages/devtools_app/lib/src/shared/framework/screen.dart
@@ -290,9 +290,6 @@
/// Whether to show the console for this screen.
bool showConsole(EmbedMode embedMode) => false;
- /// Whether to show the AI Assistant for this screen.
- bool showAiAssistant() => false;
-
/// Which keyboard shortcuts should be enabled for this screen.
ShortcutsConfiguration buildKeyboardShortcuts(BuildContext context) =>
ShortcutsConfiguration.empty();
diff --git a/packages/devtools_app/test/framework/scaffold/scaffold_ai_assistant_test.dart b/packages/devtools_app/test/framework/scaffold/scaffold_ai_assistant_test.dart
deleted file mode 100644
index 8b3d04f..0000000
--- a/packages/devtools_app/test/framework/scaffold/scaffold_ai_assistant_test.dart
+++ /dev/null
@@ -1,159 +0,0 @@
-// Copyright 2025 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/devtools_app.dart';
-import 'package:devtools_app/src/framework/scaffold/scaffold.dart';
-import 'package:devtools_app/src/shared/ai_assistant/widgets/ai_assistant_pane.dart';
-import 'package:devtools_app/src/shared/feature_flags.dart';
-import 'package:devtools_app/src/shared/framework/framework_controller.dart';
-import 'package:devtools_app/src/shared/managers/survey.dart';
-import 'package:devtools_app_shared/service.dart';
-import 'package:devtools_app_shared/ui.dart';
-import 'package:devtools_app_shared/utils.dart';
-import 'package:devtools_test/devtools_test.dart';
-import 'package:devtools_test/helpers.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:mockito/mockito.dart';
-
-void main() {
- late MockServiceConnectionManager mockServiceConnection;
- late MockServiceManager mockServiceManager;
-
- setUp(() {
- mockServiceConnection = createMockServiceConnectionWithDefaults();
- mockServiceManager =
- mockServiceConnection.serviceManager as MockServiceManager;
- when(
- mockServiceManager.connectedState,
- ).thenReturn(ValueNotifier<ConnectedState>(const ConnectedState(false)));
- final mockErrorBadgeManager = MockErrorBadgeManager();
- when(
- mockServiceConnection.errorBadgeManager,
- ).thenReturn(mockErrorBadgeManager);
- when(
- mockErrorBadgeManager.errorCountNotifier(any),
- ).thenReturn(ValueNotifier<int>(0));
-
- setGlobal(ServiceConnectionManager, mockServiceConnection);
- setGlobal(FrameworkController, FrameworkController());
- setGlobal(SurveyService, SurveyService());
- setGlobal(IdeTheme, IdeTheme());
- setGlobal(NotificationService, NotificationService());
- setGlobal(BannerMessagesController, BannerMessagesController());
- });
-
- Future<void> pumpScaffold(
- WidgetTester tester, {
- required Screen screen,
- bool withConnectedApp = true,
- bool withOfflineData = false,
- }) async {
- if (withOfflineData) {
- final offlineController = MockOfflineDataController();
- offlineController.showingOfflineData.value = true;
- setGlobal(OfflineDataController, offlineController);
- }
-
- MockConnectedApp? connectedApp;
- if (withConnectedApp) {
- connectedApp = MockConnectedApp();
- mockConnectedApp(connectedApp);
- }
- when(
- mockServiceManager.connectedAppInitialized,
- ).thenReturn(withConnectedApp);
- when(mockServiceManager.connectedApp).thenReturn(connectedApp);
-
- await tester.pumpWidget(
- wrapWithControllers(
- DevToolsScaffold(screens: [screen]),
- analytics: AnalyticsController(
- enabled: false,
- shouldShowConsentMessage: false,
- consentMessage: 'fake message',
- ),
- ),
- );
- }
-
- group('AI Assistant pane', () {
- testWidgets('is visible for supported screens', (
- WidgetTester tester,
- ) async {
- FeatureFlags.aiAssistant.setEnabledForTests(true);
-
- await pumpScaffold(tester, screen: const _TestScreenWithAi());
-
- expect(find.byType(AiAssistantPane), findsOneWidget);
- });
-
- testWidgets('is not visible for unsupported screens', (
- WidgetTester tester,
- ) async {
- FeatureFlags.aiAssistant.setEnabledForTests(true);
-
- await pumpScaffold(tester, screen: const _TestScreenWithoutAi());
-
- expect(find.byType(AiAssistantPane), findsNothing);
- });
-
- testWidgets('is not visible when app is not connected', (
- WidgetTester tester,
- ) async {
- FeatureFlags.aiAssistant.setEnabledForTests(true);
-
- await pumpScaffold(
- tester,
- screen: const _TestScreenWithAi(),
- withConnectedApp: false,
- );
-
- expect(find.byType(AiAssistantPane), findsNothing);
- });
-
- testWidgets('is not visible when feature flag is disabled', (
- WidgetTester tester,
- ) async {
- FeatureFlags.aiAssistant.setEnabledForTests(false);
-
- await pumpScaffold(tester, screen: const _TestScreenWithAi());
-
- expect(find.byType(AiAssistantPane), findsNothing);
- });
-
- testWidgets('is not visible when in offline mode', (
- WidgetTester tester,
- ) async {
- FeatureFlags.aiAssistant.setEnabledForTests(true);
-
- await pumpScaffold(
- tester,
- screen: const _TestScreenWithAi(),
- withOfflineData: true,
- );
-
- expect(find.byType(AiAssistantPane), findsNothing);
- });
- });
-}
-
-class _TestScreenWithAi extends Screen {
- const _TestScreenWithAi()
- : super('test_screen_with_ai', showFloatingDebuggerControls: false);
-
- @override
- bool showAiAssistant() => true;
-
- @override
- Widget buildScreenBody(BuildContext context) => const SizedBox();
-}
-
-class _TestScreenWithoutAi extends Screen {
- const _TestScreenWithoutAi()
- : super('test_screen_without_ai', showFloatingDebuggerControls: false);
-
- @override
- Widget buildScreenBody(BuildContext context) => const SizedBox();
-}
diff --git a/packages/devtools_app/test/shared/primitives/feature_flags_test.dart b/packages/devtools_app/test/shared/primitives/feature_flags_test.dart
index 762e03c..f1d6665 100644
--- a/packages/devtools_app/test/shared/primitives/feature_flags_test.dart
+++ b/packages/devtools_app/test/shared/primitives/feature_flags_test.dart
@@ -20,7 +20,6 @@
expect(FeatureFlags.devToolsExtensions.isEnabled, isExternalBuild);
expect(FeatureFlags.dapDebugging.isEnabled, false);
expect(FeatureFlags.inspectorV2.isEnabled, true);
- expect(FeatureFlags.aiAssistant.isEnabled, false);
});
group('FlutterChannelFeatureFlag', () {