Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
// 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/bottom_pane.dart';
import 'package:devtools_app/src/screens/memory/panes/control/widgets/primary_controls.dart';
import 'package:devtools_app/src/screens/memory/panes/diff/widgets/snapshot_list.dart';
import 'package:devtools_app/src/shared/console/widgets/console_pane.dart';
import 'package:devtools_test/helpers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
Expand Down Expand Up @@ -45,7 +45,7 @@ Future<void> prepareMemoryUI(
// but not too big to make classes in snapshot hidden.
const dragDistance = -320.0;
await tester.drag(
find.byType(ConsolePaneHeader),
find.byKey(BottomPane.splitterKey),
const Offset(0, dragDistance),
);
await tester.pumpAndSettle();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// 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:flutter/material.dart';

import '../../shared/ui/tab.dart';

/// A widget that displays a tabbed view at the bottom of the DevTools screen.
///
/// This widget is used to host views like the console and the AI Assistant.
class BottomPane extends StatelessWidget {
const BottomPane({super.key, required this.screenId, required this.tabs})
: assert(tabs.length > 0);

static const splitterKey = Key('Bottom Pane Splitter');

final String screenId;
final List<TabbedPane> tabs;

@override
Widget build(BuildContext context) {
return AnalyticsTabbedView(
gaScreen: screenId,
tabs: tabs
.map((tabbedPane) => (tab: tabbedPane.tab, tabView: tabbedPane))
.toList(),
staticSingleTab: true,
);
}
}

/// An interface for a widget that should be displayed as a tab in the
/// [BottomPane].
abstract class TabbedPane implements Widget {
/// The tab to display for this pane.
DevToolsTab get tab;
}
38 changes: 25 additions & 13 deletions packages/devtools_app/lib/src/framework/scaffold/scaffold.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ 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';
Expand All @@ -25,6 +26,7 @@ import '../../shared/primitives/query_parameters.dart';
import '../../shared/title.dart';
import 'about_dialog.dart';
import 'app_bar.dart';
import 'bottom_pane.dart';
import 'report_feedback_button.dart';
import 'settings_dialog.dart';
import 'status_line.dart';
Expand Down Expand Up @@ -310,10 +312,16 @@ class DevToolsScaffoldState extends State<DevToolsScaffold>
return Provider<ImportController>.value(
value: _importController,
builder: (context, _) {
final showConsole =
final isConnectedAppView =
serviceConnection.serviceManager.connectedAppInitialized &&
!offlineDataController.showingOfflineData.value &&
_currentScreen.showConsole(widget.embedMode);
!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 =
Expand Down Expand Up @@ -345,20 +353,24 @@ class DevToolsScaffoldState extends State<DevToolsScaffold>
body: OutlineDecoration.onlyTop(
child: Padding(
padding: widget.appPadding,
child: showConsole
child: showBottomPane
? SplitPane(
axis: Axis.vertical,
splitters: [ConsolePaneHeader()],
initialFractions: const [0.8, 0.2],
children: [
Padding(
padding: const EdgeInsets.only(
bottom: intermediateSpacing,
),
child: content,
splitters: const [
DefaultSplitter(
key: BottomPane.splitterKey,
isHorizontal: true,
),
RoundedOutlinedBorder.onlyBottom(
child: const ConsolePane(),
],
children: [
content,
BottomPane(
screenId: _currentScreen.screenId,
tabs: [
if (showConsole) const ConsolePane(),
if (showAiAssistant) const AiAssistantPane(),
],
),
],
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class InspectorScreen extends Screen {
@override
bool showConsole(EmbedMode embedMode) => !embedMode.embedded;

@override
bool showAiAssistant() => true;

@override
String get docPageId => screenId;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ class NetworkScreen extends Screen {
@override
String get docPageId => screenId;

@override
bool showAiAssistant() => true;

@override
Widget buildScreenBody(BuildContext context) => const NetworkScreenBody();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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:flutter/material.dart';

import '../../../framework/scaffold/bottom_pane.dart';
import '../../ui/tab.dart';

class AiAssistantPane extends StatelessWidget implements TabbedPane {
const AiAssistantPane({super.key});

@override
DevToolsTab get tab =>
DevToolsTab.create(tabName: _tabName, gaPrefix: _gaPrefix);

static const _tabName = 'AI Assistant';
Copy link
Contributor

Choose a reason for hiding this comment

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

Has this name been vetted by... someone? Not sure who 😅 . But I feel like Product or UX might want to sign off on a branding here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Since it's behind an experiment flag it can easily be changed (this will not be visible to users until we enable the flag).


static const _gaPrefix = 'aiAssistant';

@override
Widget build(BuildContext context) {
return const Column(
children: [
Expanded(child: Center(child: Text('TODO: Implement AI Assistant.'))),
],
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,37 @@ import 'package:devtools_app_shared/ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import '../../../framework/scaffold/bottom_pane.dart';
import '../../globals.dart';
import '../../ui/common_widgets.dart';
import '../../ui/tab.dart';
import '../console.dart';
import '../console_service.dart';
import 'evaluate.dart';
import 'help_dialog.dart';

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

class ConsolePaneHeader extends AreaPaneHeader {
ConsolePaneHeader({super.key})
: super(
title: const Text('Console'),
roundedTopBorder: true,
actions: [
const ConsoleHelpLink(),
const SizedBox(width: densePadding),
CopyToClipboardControl(
dataProvider: () =>
serviceConnection.consoleService.stdio.value.join('\n'),
buttonKey: ConsolePane.copyToClipboardButtonKey,
),
const SizedBox(width: densePadding),
DeleteControl(
buttonKey: ConsolePane.clearStdioButtonKey,
tooltip: 'Clear console output',
onPressed: () => serviceConnection.consoleService.clearStdio(),
),
],
);
}

/// Display the stdout and stderr output from the process under debug.
class ConsolePane extends StatelessWidget {
class ConsolePane extends StatelessWidget implements TabbedPane {
const ConsolePane({super.key});

static const copyToClipboardButtonKey = Key(
'console_copy_to_clipboard_button',
);
static const clearStdioButtonKey = Key('console_clear_stdio_button');

static const _tabName = 'Console';

static const _gaPrefix = 'consolePane';

@override
DevToolsTab get tab => DevToolsTab.create(
tabName: _tabName,
gaPrefix: _gaPrefix,
trailing: const _ConsoleActions(),
);

ValueListenable<List<ConsoleLine>> get stdio =>
serviceConnection.consoleService.stdio;

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

return Column(
children: [
Expanded(
child: Console(lines: stdio, footer: footer),
return Console(lines: stdio, footer: footer);
}
}

class _ConsoleActions extends StatelessWidget {
const _ConsoleActions();

@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const ConsoleHelpLink(),
const SizedBox(width: densePadding),
CopyToClipboardControl(
dataProvider: () =>
serviceConnection.consoleService.stdio.value.join('\n'),
buttonKey: ConsolePane.copyToClipboardButtonKey,
),
const SizedBox(width: densePadding),
DeleteControl(
buttonKey: ConsolePane.clearStdioButtonKey,
tooltip: 'Clear console output',
onPressed: () => serviceConnection.consoleService.clearStdio(),
),
],
);
Expand Down
9 changes: 9 additions & 0 deletions packages/devtools_app/lib/src/shared/feature_flags.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ 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
Expand All @@ -95,6 +103,7 @@ extension FeatureFlags on Never {
devToolsExtensions,
dapDebugging,
inspectorV2,
aiAssistant,
};

/// A set of all the Flutter channel feature flags for debugging purposes.
Expand Down
3 changes: 3 additions & 0 deletions packages/devtools_app/lib/src/shared/framework/screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ 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();
Expand Down
47 changes: 40 additions & 7 deletions packages/devtools_app/lib/src/shared/ui/tab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class AnalyticsTabbedView extends StatefulWidget {
this.onTabChanged,
this.initialSelectedIndex,
this.analyticsSessionIdentifier,
this.staticSingleTab = false,
}) : trailingWidgets = List.generate(
tabs.length,
(index) => tabs[index].tab.trailing ?? const SizedBox(),
Expand All @@ -106,6 +107,10 @@ class AnalyticsTabbedView extends StatefulWidget {
/// events.
final String? analyticsSessionIdentifier;

/// When there is only a single tab, whether to display that tab as a static
/// title instead of in a [TabBar].
final bool staticSingleTab;

/// Whether to send analytics events to GA.
///
/// Only set this to false if [AnalyticsTabbedView] is being used for
Expand Down Expand Up @@ -202,13 +207,10 @@ class _AnalyticsTabbedViewState extends State<AnalyticsTabbedView>
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: TabBar(
labelColor: Theme.of(context).colorScheme.onSurface,
controller: _tabController,
tabs: widget.tabs.map((t) => t.tab).toList(),
isScrollable: true,
),
_AnalyticsTabBar(
tabs: widget.tabs.map((t) => t.tab).toList(),
tabController: _tabController,
staticSingleTab: widget.staticSingleTab,
),
widget.trailingWidgets[_currentTabControllerIndex],
],
Expand All @@ -233,3 +235,34 @@ class _AnalyticsTabbedViewState extends State<AnalyticsTabbedView>
);
}
}

/// A [TabBar] used by [AnalyticsTabbedView].
///
/// When there is only a single tab and [staticSingleTab] is true, this tab bar
/// will be displayed as a static title.
class _AnalyticsTabBar extends StatelessWidget {
const _AnalyticsTabBar({
required this.tabs,
required this.tabController,
required this.staticSingleTab,
});

static const _tabPadding = 14.0;

final List<DevToolsTab> tabs;
final TabController? tabController;
final bool staticSingleTab;

@override
Widget build(BuildContext context) => (staticSingleTab && tabs.length == 1)
? Padding(
padding: const EdgeInsets.symmetric(horizontal: _tabPadding),
child: tabs.first,
)
: TabBar(
labelColor: Theme.of(context).colorScheme.onSurface,
controller: tabController,
tabs: tabs,
isScrollable: true,
);
}
Loading