Skip to content

Commit d2a1458

Browse files
authored
AI assistant pane is displayed for supported screens when the AI assistant feature flag is enabled (#9591)
1 parent 203c62e commit d2a1458

File tree

16 files changed

+370
-61
lines changed

16 files changed

+370
-61
lines changed

packages/devtools_app/integration_test/test/live_connection/memory_screen_helpers.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
44

55
import 'package:devtools_app/devtools_app.dart';
6+
import 'package:devtools_app/src/framework/scaffold/bottom_pane.dart';
67
import 'package:devtools_app/src/screens/memory/panes/control/widgets/primary_controls.dart';
78
import 'package:devtools_app/src/screens/memory/panes/diff/widgets/snapshot_list.dart';
8-
import 'package:devtools_app/src/shared/console/widgets/console_pane.dart';
99
import 'package:devtools_test/helpers.dart';
1010
import 'package:flutter/material.dart';
1111
import 'package:flutter_test/flutter_test.dart';
@@ -45,7 +45,7 @@ Future<void> prepareMemoryUI(
4545
// but not too big to make classes in snapshot hidden.
4646
const dragDistance = -320.0;
4747
await tester.drag(
48-
find.byType(ConsolePaneHeader),
48+
find.byKey(BottomPane.splitterKey),
4949
const Offset(0, dragDistance),
5050
);
5151
await tester.pumpAndSettle();
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2025 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
4+
5+
import 'package:flutter/material.dart';
6+
7+
import '../../shared/ui/tab.dart';
8+
9+
/// A widget that displays a tabbed view at the bottom of the DevTools screen.
10+
///
11+
/// This widget is used to host views like the console and the AI Assistant.
12+
class BottomPane extends StatelessWidget {
13+
const BottomPane({super.key, required this.screenId, required this.tabs})
14+
: assert(tabs.length > 0);
15+
16+
static const splitterKey = Key('Bottom Pane Splitter');
17+
18+
final String screenId;
19+
final List<TabbedPane> tabs;
20+
21+
@override
22+
Widget build(BuildContext context) {
23+
return AnalyticsTabbedView(
24+
gaScreen: screenId,
25+
tabs: tabs
26+
.map((tabbedPane) => (tab: tabbedPane.tab, tabView: tabbedPane))
27+
.toList(),
28+
staticSingleTab: true,
29+
);
30+
}
31+
}
32+
33+
/// An interface for a widget that should be displayed as a tab in the
34+
/// [BottomPane].
35+
abstract class TabbedPane implements Widget {
36+
/// The tab to display for this pane.
37+
DevToolsTab get tab;
38+
}

packages/devtools_app/lib/src/framework/scaffold/scaffold.dart

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:provider/provider.dart';
1111
import '../../app.dart';
1212
import '../../extensions/extension_settings.dart';
1313
import '../../screens/debugger/debugger_screen.dart';
14+
import '../../shared/ai_assistant/widgets/ai_assistant_pane.dart';
1415
import '../../shared/analytics/prompt.dart';
1516
import '../../shared/config_specific/drag_and_drop/drag_and_drop.dart';
1617
import '../../shared/config_specific/import_export/import_export.dart';
@@ -25,6 +26,7 @@ import '../../shared/primitives/query_parameters.dart';
2526
import '../../shared/title.dart';
2627
import 'about_dialog.dart';
2728
import 'app_bar.dart';
29+
import 'bottom_pane.dart';
2830
import 'report_feedback_button.dart';
2931
import 'settings_dialog.dart';
3032
import 'status_line.dart';
@@ -310,10 +312,16 @@ class DevToolsScaffoldState extends State<DevToolsScaffold>
310312
return Provider<ImportController>.value(
311313
value: _importController,
312314
builder: (context, _) {
313-
final showConsole =
315+
final isConnectedAppView =
314316
serviceConnection.serviceManager.connectedAppInitialized &&
315-
!offlineDataController.showingOfflineData.value &&
316-
_currentScreen.showConsole(widget.embedMode);
317+
!offlineDataController.showingOfflineData.value;
318+
final showConsole =
319+
isConnectedAppView && _currentScreen.showConsole(widget.embedMode);
320+
final showAiAssistant =
321+
FeatureFlags.aiAssistant.isEnabled &&
322+
isConnectedAppView &&
323+
_currentScreen.showAiAssistant();
324+
final showBottomPane = showConsole || showAiAssistant;
317325
final containsSingleSimpleScreen =
318326
widget.screens.length == 1 && widget.screens.first is SimpleScreen;
319327
final showAppBar =
@@ -345,20 +353,24 @@ class DevToolsScaffoldState extends State<DevToolsScaffold>
345353
body: OutlineDecoration.onlyTop(
346354
child: Padding(
347355
padding: widget.appPadding,
348-
child: showConsole
356+
child: showBottomPane
349357
? SplitPane(
350358
axis: Axis.vertical,
351-
splitters: [ConsolePaneHeader()],
352359
initialFractions: const [0.8, 0.2],
353-
children: [
354-
Padding(
355-
padding: const EdgeInsets.only(
356-
bottom: intermediateSpacing,
357-
),
358-
child: content,
360+
splitters: const [
361+
DefaultSplitter(
362+
key: BottomPane.splitterKey,
363+
isHorizontal: true,
359364
),
360-
RoundedOutlinedBorder.onlyBottom(
361-
child: const ConsolePane(),
365+
],
366+
children: [
367+
content,
368+
BottomPane(
369+
screenId: _currentScreen.screenId,
370+
tabs: [
371+
if (showConsole) const ConsolePane(),
372+
if (showAiAssistant) const AiAssistantPane(),
373+
],
362374
),
363375
],
364376
)

packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ class InspectorScreen extends Screen {
2525
@override
2626
bool showConsole(EmbedMode embedMode) => !embedMode.embedded;
2727

28+
@override
29+
bool showAiAssistant() => true;
30+
2831
@override
2932
String get docPageId => screenId;
3033

packages/devtools_app/lib/src/screens/network/network_screen.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ class NetworkScreen extends Screen {
3939
@override
4040
String get docPageId => screenId;
4141

42+
@override
43+
bool showAiAssistant() => true;
44+
4245
@override
4346
Widget buildScreenBody(BuildContext context) => const NetworkScreenBody();
4447

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright 2025 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
4+
5+
import 'package:flutter/material.dart';
6+
7+
import '../../../framework/scaffold/bottom_pane.dart';
8+
import '../../ui/tab.dart';
9+
10+
class AiAssistantPane extends StatelessWidget implements TabbedPane {
11+
const AiAssistantPane({super.key});
12+
13+
@override
14+
DevToolsTab get tab =>
15+
DevToolsTab.create(tabName: _tabName, gaPrefix: _gaPrefix);
16+
17+
static const _tabName = 'AI Assistant';
18+
19+
static const _gaPrefix = 'aiAssistant';
20+
21+
@override
22+
Widget build(BuildContext context) {
23+
return const Column(
24+
children: [
25+
Expanded(child: Center(child: Text('TODO: Implement AI Assistant.'))),
26+
],
27+
);
28+
}
29+
}

packages/devtools_app/lib/src/shared/console/widgets/console_pane.dart

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,47 +6,37 @@ import 'package:devtools_app_shared/ui.dart';
66
import 'package:flutter/foundation.dart';
77
import 'package:flutter/material.dart';
88

9+
import '../../../framework/scaffold/bottom_pane.dart';
910
import '../../globals.dart';
1011
import '../../ui/common_widgets.dart';
12+
import '../../ui/tab.dart';
1113
import '../console.dart';
1214
import '../console_service.dart';
1315
import 'evaluate.dart';
1416
import 'help_dialog.dart';
1517

1618
// TODO(devoncarew): Show some small UI indicator when we receive stdout/stderr.
1719

18-
class ConsolePaneHeader extends AreaPaneHeader {
19-
ConsolePaneHeader({super.key})
20-
: super(
21-
title: const Text('Console'),
22-
roundedTopBorder: true,
23-
actions: [
24-
const ConsoleHelpLink(),
25-
const SizedBox(width: densePadding),
26-
CopyToClipboardControl(
27-
dataProvider: () =>
28-
serviceConnection.consoleService.stdio.value.join('\n'),
29-
buttonKey: ConsolePane.copyToClipboardButtonKey,
30-
),
31-
const SizedBox(width: densePadding),
32-
DeleteControl(
33-
buttonKey: ConsolePane.clearStdioButtonKey,
34-
tooltip: 'Clear console output',
35-
onPressed: () => serviceConnection.consoleService.clearStdio(),
36-
),
37-
],
38-
);
39-
}
40-
4120
/// Display the stdout and stderr output from the process under debug.
42-
class ConsolePane extends StatelessWidget {
21+
class ConsolePane extends StatelessWidget implements TabbedPane {
4322
const ConsolePane({super.key});
4423

4524
static const copyToClipboardButtonKey = Key(
4625
'console_copy_to_clipboard_button',
4726
);
4827
static const clearStdioButtonKey = Key('console_clear_stdio_button');
4928

29+
static const _tabName = 'Console';
30+
31+
static const _gaPrefix = 'consolePane';
32+
33+
@override
34+
DevToolsTab get tab => DevToolsTab.create(
35+
tabName: _tabName,
36+
gaPrefix: _gaPrefix,
37+
trailing: const _ConsoleActions(),
38+
);
39+
5040
ValueListenable<List<ConsoleLine>> get stdio =>
5141
serviceConnection.consoleService.stdio;
5242

@@ -61,10 +51,30 @@ class ConsolePane extends StatelessWidget {
6151
footer = const ExpressionEvalField();
6252
}
6353

64-
return Column(
65-
children: [
66-
Expanded(
67-
child: Console(lines: stdio, footer: footer),
54+
return Console(lines: stdio, footer: footer);
55+
}
56+
}
57+
58+
class _ConsoleActions extends StatelessWidget {
59+
const _ConsoleActions();
60+
61+
@override
62+
Widget build(BuildContext context) {
63+
return Row(
64+
mainAxisSize: MainAxisSize.min,
65+
children: <Widget>[
66+
const ConsoleHelpLink(),
67+
const SizedBox(width: densePadding),
68+
CopyToClipboardControl(
69+
dataProvider: () =>
70+
serviceConnection.consoleService.stdio.value.join('\n'),
71+
buttonKey: ConsolePane.copyToClipboardButtonKey,
72+
),
73+
const SizedBox(width: densePadding),
74+
DeleteControl(
75+
buttonKey: ConsolePane.clearStdioButtonKey,
76+
tooltip: 'Clear console output',
77+
onPressed: () => serviceConnection.consoleService.clearStdio(),
6878
),
6979
],
7080
);

packages/devtools_app/lib/src/shared/feature_flags.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ extension FeatureFlags on Never {
8585
enabled: true,
8686
);
8787

88+
/// Flag to enable the AI Assistant.
89+
///
90+
/// https://github.com/flutter/devtools/issues/9590
91+
static final aiAssistant = BooleanFeatureFlag(
92+
name: 'aiAssistant',
93+
enabled: enableExperiments,
94+
);
95+
8896
/// A set of all the boolean feature flags for debugging purposes.
8997
///
9098
/// When adding a new boolean flag, you are responsible for adding it to this
@@ -95,6 +103,7 @@ extension FeatureFlags on Never {
95103
devToolsExtensions,
96104
dapDebugging,
97105
inspectorV2,
106+
aiAssistant,
98107
};
99108

100109
/// A set of all the Flutter channel feature flags for debugging purposes.

packages/devtools_app/lib/src/shared/framework/screen.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,9 @@ abstract class Screen {
290290
/// Whether to show the console for this screen.
291291
bool showConsole(EmbedMode embedMode) => false;
292292

293+
/// Whether to show the AI Assistant for this screen.
294+
bool showAiAssistant() => false;
295+
293296
/// Which keyboard shortcuts should be enabled for this screen.
294297
ShortcutsConfiguration buildKeyboardShortcuts(BuildContext context) =>
295298
ShortcutsConfiguration.empty();

packages/devtools_app/lib/src/shared/ui/tab.dart

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class AnalyticsTabbedView extends StatefulWidget {
8181
this.onTabChanged,
8282
this.initialSelectedIndex,
8383
this.analyticsSessionIdentifier,
84+
this.staticSingleTab = false,
8485
}) : trailingWidgets = List.generate(
8586
tabs.length,
8687
(index) => tabs[index].tab.trailing ?? const SizedBox(),
@@ -106,6 +107,10 @@ class AnalyticsTabbedView extends StatefulWidget {
106107
/// events.
107108
final String? analyticsSessionIdentifier;
108109

110+
/// When there is only a single tab, whether to display that tab as a static
111+
/// title instead of in a [TabBar].
112+
final bool staticSingleTab;
113+
109114
/// Whether to send analytics events to GA.
110115
///
111116
/// Only set this to false if [AnalyticsTabbedView] is being used for
@@ -202,13 +207,10 @@ class _AnalyticsTabbedViewState extends State<AnalyticsTabbedView>
202207
child: Row(
203208
mainAxisAlignment: MainAxisAlignment.spaceBetween,
204209
children: [
205-
Expanded(
206-
child: TabBar(
207-
labelColor: Theme.of(context).colorScheme.onSurface,
208-
controller: _tabController,
209-
tabs: widget.tabs.map((t) => t.tab).toList(),
210-
isScrollable: true,
211-
),
210+
_AnalyticsTabBar(
211+
tabs: widget.tabs.map((t) => t.tab).toList(),
212+
tabController: _tabController,
213+
staticSingleTab: widget.staticSingleTab,
212214
),
213215
widget.trailingWidgets[_currentTabControllerIndex],
214216
],
@@ -233,3 +235,34 @@ class _AnalyticsTabbedViewState extends State<AnalyticsTabbedView>
233235
);
234236
}
235237
}
238+
239+
/// A [TabBar] used by [AnalyticsTabbedView].
240+
///
241+
/// When there is only a single tab and [staticSingleTab] is true, this tab bar
242+
/// will be displayed as a static title.
243+
class _AnalyticsTabBar extends StatelessWidget {
244+
const _AnalyticsTabBar({
245+
required this.tabs,
246+
required this.tabController,
247+
required this.staticSingleTab,
248+
});
249+
250+
static const _tabPadding = 14.0;
251+
252+
final List<DevToolsTab> tabs;
253+
final TabController? tabController;
254+
final bool staticSingleTab;
255+
256+
@override
257+
Widget build(BuildContext context) => (staticSingleTab && tabs.length == 1)
258+
? Padding(
259+
padding: const EdgeInsets.symmetric(horizontal: _tabPadding),
260+
child: tabs.first,
261+
)
262+
: TabBar(
263+
labelColor: Theme.of(context).colorScheme.onSurface,
264+
controller: tabController,
265+
tabs: tabs,
266+
isScrollable: true,
267+
);
268+
}

0 commit comments

Comments
 (0)