diff --git a/lib/app/app_shell_desktop.dart b/lib/app/app_shell_desktop.dart index 3b4a08ec..57aa0cfc 100644 --- a/lib/app/app_shell_desktop.dart +++ b/lib/app/app_shell_desktop.dart @@ -379,6 +379,11 @@ class _AppShellState extends State { controller, visibleExecutionTargets, ), + onReturnToAssistant: () { + controller.navigateTo( + WorkspaceDestination.assistant, + ); + }, onSelectTask: (sessionKey) async { controller.navigateTo( WorkspaceDestination.assistant, diff --git a/lib/widgets/sidebar_navigation.dart b/lib/widgets/sidebar_navigation.dart index cce3d922..8b8c2fcb 100644 --- a/lib/widgets/sidebar_navigation.dart +++ b/lib/widgets/sidebar_navigation.dart @@ -46,6 +46,7 @@ class SidebarNavigation extends StatelessWidget { this.assistantSkillCount = 0, this.onRefreshTasks, this.onCreateTask, + this.onReturnToAssistant, this.onSelectTask, this.onArchiveTask, this.onRenameTask, @@ -81,6 +82,7 @@ class SidebarNavigation extends StatelessWidget { final int assistantSkillCount; final Future Function()? onRefreshTasks; final Future Function()? onCreateTask; + final VoidCallback? onReturnToAssistant; final Future Function(String sessionKey)? onSelectTask; final Future Function(String sessionKey)? onArchiveTask; final Future Function(String sessionKey, String title)? onRenameTask; @@ -123,6 +125,7 @@ class SidebarNavigation extends StatelessWidget { if (!isCollapsed) Expanded( child: SidebarTaskSection( + currentSection: currentSection, items: taskItems, visibleExecutionTargets: visibleExecutionTargets, skillCount: assistantSkillCount, @@ -130,6 +133,7 @@ class SidebarNavigation extends StatelessWidget { onCycleSidebarState: onCycleSidebarState, onRefreshTasks: onRefreshTasks, onCreateTask: onCreateTask, + onReturnToAssistant: onReturnToAssistant, onSelectTask: onSelectTask, onArchiveTask: onArchiveTask, onRenameTask: onRenameTask, diff --git a/lib/widgets/sidebar_navigation_task_section.dart b/lib/widgets/sidebar_navigation_task_section.dart index 1c9a1c34..9a954c38 100644 --- a/lib/widgets/sidebar_navigation_task_section.dart +++ b/lib/widgets/sidebar_navigation_task_section.dart @@ -25,6 +25,7 @@ class SidebarTaskItem { class SidebarTaskSection extends StatefulWidget { const SidebarTaskSection({ super.key, + required this.currentSection, required this.items, required this.visibleExecutionTargets, required this.skillCount, @@ -32,11 +33,13 @@ class SidebarTaskSection extends StatefulWidget { required this.onCycleSidebarState, this.onRefreshTasks, this.onCreateTask, + this.onReturnToAssistant, this.onSelectTask, this.onArchiveTask, this.onRenameTask, }); + final WorkspaceDestination currentSection; final List items; final List visibleExecutionTargets; final int skillCount; @@ -44,6 +47,7 @@ class SidebarTaskSection extends StatefulWidget { final VoidCallback onCycleSidebarState; final Future Function()? onRefreshTasks; final Future Function()? onCreateTask; + final VoidCallback? onReturnToAssistant; final Future Function(String sessionKey)? onSelectTask; final Future Function(String sessionKey)? onArchiveTask; final Future Function(String sessionKey, String title)? onRenameTask; @@ -157,22 +161,26 @@ class _SidebarTaskSectionState extends State { ), Padding( padding: const EdgeInsets.fromLTRB(4, 0, 4, 8), - child: FilledButton.tonalIcon( - key: const Key('workspace-sidebar-new-task-button'), - onPressed: widget.onCreateTask == null - ? null - : () async { - await widget.onCreateTask!(); - }, - icon: const Icon(Icons.edit_note_rounded), - label: Text(appText('新对话', 'New conversation')), - style: FilledButton.styleFrom( - minimumSize: const Size(0, 40), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - ), + child: widget.currentSection == WorkspaceDestination.settings + ? _SidebarBackToAssistantButton( + onPressed: widget.onReturnToAssistant, + ) + : FilledButton.tonalIcon( + key: const Key('workspace-sidebar-new-task-button'), + onPressed: widget.onCreateTask == null + ? null + : () async { + await widget.onCreateTask!(); + }, + icon: const Icon(Icons.edit_note_rounded), + label: Text(appText('新对话', 'New conversation')), + style: FilledButton.styleFrom( + minimumSize: const Size(0, 40), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), ), Padding( padding: const EdgeInsets.fromLTRB(4, 4, 4, 6), @@ -341,6 +349,66 @@ class _SidebarTaskSectionState extends State { } } +class _SidebarBackToAssistantButton extends StatelessWidget { + const _SidebarBackToAssistantButton({required this.onPressed}); + + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + final palette = context.palette; + final theme = Theme.of(context); + return Material( + color: Colors.transparent, + child: InkWell( + key: const Key('workspace-sidebar-back-to-chat-button'), + onTap: onPressed, + borderRadius: BorderRadius.circular(18), + child: Ink( + height: 56, + decoration: BoxDecoration( + color: palette.surfaceSecondary, + borderRadius: BorderRadius.circular(18), + border: Border.all(color: palette.strokeSoft), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: palette.textPrimary, width: 1.8), + ), + child: Icon( + Icons.arrow_back_rounded, + size: 24, + color: palette.textPrimary, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + appText('返回聊天', 'Back to chat'), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + color: palette.textPrimary, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} + class _SidebarTaskGroup { const _SidebarTaskGroup({required this.executionTarget, required this.items}); diff --git a/test/golden/goldens/sidebar_navigation_settings_back_to_chat.png b/test/golden/goldens/sidebar_navigation_settings_back_to_chat.png new file mode 100644 index 00000000..09afa0b4 Binary files /dev/null and b/test/golden/goldens/sidebar_navigation_settings_back_to_chat.png differ diff --git a/test/golden/sidebar_navigation_settings_back_to_chat_golden_test.dart b/test/golden/sidebar_navigation_settings_back_to_chat_golden_test.dart new file mode 100644 index 00000000..b519f63b --- /dev/null +++ b/test/golden/sidebar_navigation_settings_back_to_chat_golden_test.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:xworkmate/i18n/app_language.dart'; +import 'package:xworkmate/models/app_models.dart'; +import 'package:xworkmate/widgets/sidebar_navigation.dart'; + +import '../helpers/golden_test_bootstrap.dart'; + +void main() { + setUpAll(() async { + await loadGoldenFonts(); + }); + + testGoldens('settings sidebar shows back to chat action', (tester) async { + await pumpGoldenApp( + tester, + Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 344, + height: 920, + child: SidebarNavigation( + currentSection: WorkspaceDestination.settings, + sidebarState: AppSidebarState.expanded, + appLanguage: AppLanguage.zh, + themeMode: ThemeMode.light, + onSectionChanged: (_) {}, + onToggleLanguage: () {}, + onCycleSidebarState: () {}, + onExpandFromCollapsed: () {}, + onOpenHome: () {}, + onOpenAccount: () {}, + onOpenThemeToggle: () {}, + onReturnToAssistant: () {}, + accountName: 'Tester', + accountSubtitle: 'Workspace', + onToggleAccountWorkspaceFollowed: () async {}, + ), + ), + ), + size: const Size(400, 960), + ); + + await screenMatchesGolden( + tester, + 'sidebar_navigation_settings_back_to_chat', + ); + }); +} diff --git a/test/widgets/sidebar_navigation_suite.dart b/test/widgets/sidebar_navigation_suite.dart index 14dbffbe..74c943a8 100644 --- a/test/widgets/sidebar_navigation_suite.dart +++ b/test/widgets/sidebar_navigation_suite.dart @@ -256,6 +256,10 @@ void main() { find.byKey(const Key('workspace-sidebar-new-task-button')), findsOneWidget, ); + expect( + find.byKey(const Key('workspace-sidebar-back-to-chat-button')), + findsNothing, + ); expect(find.text('任务列表'), findsOneWidget); expect(find.text('自动化'), findsNothing); expect(find.text('MCP Hub'), findsNothing); @@ -269,6 +273,55 @@ void main() { }, ); + testWidgets('SidebarNavigation shows back to chat action on settings page', ( + WidgetTester tester, + ) async { + var returnedToAssistant = 0; + + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.light(), + home: Scaffold( + body: SidebarNavigation( + currentSection: WorkspaceDestination.settings, + sidebarState: AppSidebarState.expanded, + appLanguage: AppLanguage.zh, + themeMode: ThemeMode.light, + onSectionChanged: (_) {}, + onToggleLanguage: () {}, + onCycleSidebarState: () {}, + onExpandFromCollapsed: () {}, + onOpenHome: () {}, + onOpenAccount: () {}, + onOpenThemeToggle: () {}, + accountName: 'Tester', + accountSubtitle: 'Workspace', + onToggleAccountWorkspaceFollowed: () async {}, + onReturnToAssistant: () => returnedToAssistant++, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect( + find.byKey(const Key('workspace-sidebar-back-to-chat-button')), + findsOneWidget, + ); + expect(find.text('返回聊天'), findsOneWidget); + expect( + find.byKey(const Key('workspace-sidebar-new-task-button')), + findsNothing, + ); + + await tester.tap( + find.byKey(const Key('workspace-sidebar-back-to-chat-button')), + ); + await tester.pumpAndSettle(); + + expect(returnedToAssistant, 1); + }); + testWidgets( 'SidebarNavigation merges local and remote tasks into one gateway group', (WidgetTester tester) async {