From a2f40cc13cf1af498e17f466cc71363e6bbd35ea Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Tue, 26 May 2026 09:34:27 +0800 Subject: [PATCH] feat(mobile): refine composer ui with minimalist modern aesthetic --- .../mobile_assistant_page_composer.dart | 39 +++++++++++----- ...troller_thread_workspace_binding_test.dart | 44 +++++++++++++++++++ .../assistant_execution_target_test.dart | 25 +++++++++++ 3 files changed, 96 insertions(+), 12 deletions(-) diff --git a/lib/features/mobile/mobile_assistant_page_composer.dart b/lib/features/mobile/mobile_assistant_page_composer.dart index 4f628fb9..e72ac52d 100644 --- a/lib/features/mobile/mobile_assistant_page_composer.dart +++ b/lib/features/mobile/mobile_assistant_page_composer.dart @@ -164,14 +164,18 @@ class MobileAssistantComposer extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ Padding( - padding: const EdgeInsets.only(right: 8, bottom: 5), - child: CircleAvatar( - radius: 18, - backgroundColor: palette.surfaceSecondary, + padding: const EdgeInsets.only(right: 8, bottom: 7), + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.light ? const Color(0xFFF4F6F8) : palette.surfaceSecondary, + shape: BoxShape.circle, + ), child: IconButton( key: const Key('mobile-assistant-composer-add-button'), padding: EdgeInsets.zero, - icon: Icon(Icons.add, color: palette.textPrimary, size: 22), + icon: Icon(Icons.add, color: palette.textPrimary, size: 20), onPressed: showConfigurationMenu, ), ), @@ -179,7 +183,7 @@ class MobileAssistantComposer extends StatelessWidget { Expanded( child: DecoratedBox( decoration: BoxDecoration( - color: palette.surfaceSecondary, + color: Theme.of(context).brightness == Brightness.light ? const Color(0xFFF4F6F8) : palette.surfaceSecondary, borderRadius: BorderRadius.circular(26), ), child: Row( @@ -214,21 +218,32 @@ class MobileAssistantComposer extends StatelessWidget { ), Padding( padding: const EdgeInsets.only(right: 12, bottom: 12), - child: Icon(Icons.mic_none, color: palette.textMuted, size: 24), + child: Icon(Icons.mic_none, color: Theme.of(context).brightness == Brightness.light ? const Color(0xFF9CA3AF) : palette.textMuted, size: 22), ), ], ), ), ), Padding( - padding: const EdgeInsets.only(left: 8, bottom: 5), - child: CircleAvatar( - radius: 18, - backgroundColor: palette.accent, + padding: const EdgeInsets.only(left: 8, bottom: 7), + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.light ? const Color(0xFF6EA8FF) : palette.accent, + shape: BoxShape.circle, + boxShadow: Theme.of(context).brightness == Brightness.light ? [ + BoxShadow( + color: const Color(0xFF6EA8FF).withValues(alpha: 0.15), + blurRadius: 4, + offset: const Offset(0, 2), + ) + ] : null, + ), child: IconButton( key: const Key('mobile-assistant-send-button'), padding: EdgeInsets.zero, - icon: const Icon(Icons.arrow_upward_rounded, color: Colors.white, size: 24), + icon: const Icon(Icons.arrow_upward_rounded, color: Colors.white, size: 20), onPressed: onSend, ), ), diff --git a/test/runtime/app_controller_thread_workspace_binding_test.dart b/test/runtime/app_controller_thread_workspace_binding_test.dart index b4f72dac..5aab6cec 100644 --- a/test/runtime/app_controller_thread_workspace_binding_test.dart +++ b/test/runtime/app_controller_thread_workspace_binding_test.dart @@ -388,6 +388,50 @@ void main() { ); }); + test( + 'mobile target selection keeps a complete remote workspace when local home is unavailable', + () async { + final controller = AppController( + environmentOverride: const {}, + initialGatewayProviderCatalog: [ + SingleAgentProvider.openclaw.copyWith( + supportedTargets: const [ + AssistantExecutionTarget.gateway, + ], + ), + ], + initialAvailableExecutionTargets: const [ + AssistantExecutionTarget.agent, + AssistantExecutionTarget.gateway, + ], + ); + addTearDown(controller.dispose); + controller.resolvedUserHomeDirectoryInternal = ''; + + await controller.ensureActiveAssistantThreadInternal(); + await controller.setAssistantExecutionTarget( + AssistantExecutionTarget.gateway, + ); + + final record = controller.taskThreadForSessionInternal( + controller.currentSessionKey, + ); + expect(controller.currentAssistantExecutionTarget.isGateway, isTrue); + expect(record?.workspaceBinding.isComplete, isTrue); + expect(record?.workspaceBinding.workspaceKind, WorkspaceKind.remoteFs); + expect( + record?.workspaceBinding.workspacePath, + contains('/threads/${controller.currentSessionKey}'), + ); + expect( + controller.assistantWorkingDirectoryForSessionInternal( + controller.currentSessionKey, + ), + record?.workspaceBinding.workspacePath, + ); + }, + ); + test('writes inline ACP artifacts into the local thread workspace', () async { final controller = AppController( environmentOverride: const {}, diff --git a/test/runtime/assistant_execution_target_test.dart b/test/runtime/assistant_execution_target_test.dart index b3279a0f..88d39edf 100644 --- a/test/runtime/assistant_execution_target_test.dart +++ b/test/runtime/assistant_execution_target_test.dart @@ -1132,6 +1132,31 @@ void main() { ); }); + test( + 'sendChatMessage runs Gateway task with remote workspace when local workspace is unavailable', + () async { + final fakeGoTaskService = _RecordingGoTaskServiceClient(); + final controller = _connectedGatewayController(fakeGoTaskService); + addTearDown(controller.dispose); + controller.resolvedUserHomeDirectoryInternal = ''; + + await controller.ensureActiveAssistantThreadInternal(); + await controller.setAssistantExecutionTarget( + AssistantExecutionTarget.gateway, + ); + await controller.sendChatMessage('mobile gateway task'); + + expect(fakeGoTaskService.requests, hasLength(1)); + final request = fakeGoTaskService.requests.single; + expect(request.target, AssistantExecutionTarget.gateway); + expect(request.provider.providerId, 'openclaw'); + expect(request.workingDirectory, startsWith('/owners/')); + expect(request.workingDirectory, contains('/threads/')); + expect(request.remoteWorkingDirectoryHint, request.workingDirectory); + expect(request.prompt, contains(request.workingDirectory)); + }, + ); + test( 'sendChatMessage forwards inline attachment content and size', () async {