diff --git a/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart b/packages/devtools_app/lib/src/framework/scaffold/scaffold.dart index 8c34b71eb82..1f2073e384a 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 'package:provider/provider.dart'; 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 @@ class DevToolsScaffoldState extends State !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 @@ class DevToolsScaffoldState extends State 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 @@ class DevToolsScaffoldState extends State 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 1ac60f68bcd..6b8a1179b36 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 @@ -22,9 +22,6 @@ class InspectorScreen extends Screen { @override bool showConsole(EmbedMode embedMode) => !embedMode.embedded; - @override - bool showAiAssistant() => true; - @override String get docPageId => screenId; 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 a9358f30f04..66843cf09b0 100644 --- a/packages/devtools_app/lib/src/screens/network/network_screen.dart +++ b/packages/devtools_app/lib/src/screens/network/network_screen.dart @@ -39,9 +39,6 @@ class NetworkScreen extends Screen { @override String get docPageId => screenId; - @override - bool showAiAssistant() => true; - @override Widget buildScreenBody(BuildContext context) => const NetworkScreenBody(); 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 879e9b2b6e4..00000000000 --- 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 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 c71223651fd..00000000000 --- 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 d8c8c5f6174..00000000000 --- 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 createState() => _AiAssistantPaneState(); -} - -class _AiAssistantPaneState extends State { - static const _baseOverscrollPadding = 125.0; - static const _spinnerHeight = 50.0; - static const _scrollDuration = Duration(milliseconds: 250); - - final _textController = TextEditingController(); - final _messages = []; - 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 _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 c30e6be29b3..a5cef0ca5c2 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 @@ extension FeatureFlags on Never { 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 @@ extension FeatureFlags on Never { 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 0cdc840653a..c3b49317b6f 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 @@ abstract class Screen { /// 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 8b3d04f0cb4..00000000000 --- 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(const ConnectedState(false))); - final mockErrorBadgeManager = MockErrorBadgeManager(); - when( - mockServiceConnection.errorBadgeManager, - ).thenReturn(mockErrorBadgeManager); - when( - mockErrorBadgeManager.errorCountNotifier(any), - ).thenReturn(ValueNotifier(0)); - - setGlobal(ServiceConnectionManager, mockServiceConnection); - setGlobal(FrameworkController, FrameworkController()); - setGlobal(SurveyService, SurveyService()); - setGlobal(IdeTheme, IdeTheme()); - setGlobal(NotificationService, NotificationService()); - setGlobal(BannerMessagesController, BannerMessagesController()); - }); - - Future 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 762e03cb9db..f1d6665f953 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 @@ void main() { expect(FeatureFlags.devToolsExtensions.isEnabled, isExternalBuild); expect(FeatureFlags.dapDebugging.isEnabled, false); expect(FeatureFlags.inspectorV2.isEnabled, true); - expect(FeatureFlags.aiAssistant.isEnabled, false); }); group('FlutterChannelFeatureFlag', () {