diff --git a/lib/features/assistant/assistant_page_composer_bar.dart b/lib/features/assistant/assistant_page_composer_bar.dart index 5aa4e37f..62d5d745 100644 --- a/lib/features/assistant/assistant_page_composer_bar.dart +++ b/lib/features/assistant/assistant_page_composer_bar.dart @@ -524,7 +524,9 @@ class ComposerBarStateInternal extends State { key: Key('assistant-gateway-provider-menu-item-openclaw'), child: Row( children: [ - Icon(Icons.cloud_outlined, size: 18), + GatewayProviderBadgeInternal( + key: Key('assistant-gateway-provider-menu-badge'), + ), SizedBox(width: 10), Expanded(child: Text(kCanonicalGatewayProviderLabel)), Icon(Icons.check_rounded, size: 18), @@ -533,7 +535,9 @@ class ComposerBarStateInternal extends State { ), ], child: ComposerToolbarChipInternal( - icon: Icons.cloud_outlined, + leading: const GatewayProviderBadgeInternal( + key: Key('assistant-gateway-provider-badge'), + ), tooltip: gatewayProviderTooltipInternal(), showChevron: true, padding: const EdgeInsets.symmetric( diff --git a/lib/features/assistant/assistant_page_composer_support.dart b/lib/features/assistant/assistant_page_composer_support.dart index 6fed462e..611d8834 100644 --- a/lib/features/assistant/assistant_page_composer_support.dart +++ b/lib/features/assistant/assistant_page_composer_support.dart @@ -246,3 +246,35 @@ class SingleAgentProviderBadgeInternal extends StatelessWidget { ); } } + +class GatewayProviderBadgeInternal extends StatelessWidget { + const GatewayProviderBadgeInternal({ + super.key, + this.size = 18, + this.fontSize = 11, + }); + + final double size; + final double fontSize; + + @override + Widget build(BuildContext context) { + final palette = context.palette; + return Container( + width: size, + height: size, + alignment: Alignment.center, + decoration: BoxDecoration( + color: palette.surfaceSecondary, + borderRadius: BorderRadius.circular(999), + border: Border.all(color: palette.strokeSoft), + ), + child: Text( + '🦞', + maxLines: 1, + overflow: TextOverflow.clip, + style: TextStyle(fontSize: fontSize, height: 1), + ), + ); + } +} diff --git a/lib/features/assistant/assistant_page_tooltip_labels.dart b/lib/features/assistant/assistant_page_tooltip_labels.dart index e6f81839..6a55633e 100644 --- a/lib/features/assistant/assistant_page_tooltip_labels.dart +++ b/lib/features/assistant/assistant_page_tooltip_labels.dart @@ -51,8 +51,8 @@ String singleAgentProviderTooltipInternal( ); String gatewayProviderTooltipInternal() => appText( - 'Gateway Provider: $kCanonicalGatewayProviderLabel', - 'Gateway Provider: $kCanonicalGatewayProviderLabel', + 'Gateway Provider: 🦞 $kCanonicalGatewayProviderLabel', + 'Gateway Provider: 🦞 $kCanonicalGatewayProviderLabel', ); String modelTooltipInternal(String modelLabel) => diff --git a/test/assistant_execution_target_picker_widget_test.dart b/test/assistant_execution_target_picker_widget_test.dart index 27f0a783..19d82a95 100644 --- a/test/assistant_execution_target_picker_widget_test.dart +++ b/test/assistant_execution_target_picker_widget_test.dart @@ -220,6 +220,11 @@ void main() { find.byKey(const Key('assistant-gateway-provider-button')), findsOneWidget, ); + expect( + find.byKey(const Key('assistant-gateway-provider-badge')), + findsOneWidget, + ); + expect(find.text('🦞'), findsOneWidget); final gatewayButton = tester.widget>( find.byKey(const Key('assistant-gateway-provider-button')), ); @@ -233,6 +238,12 @@ void main() { items.whereType>().single.value, kCanonicalGatewayProviderId, ); + final menuRow = + items.whereType>().single.child as Row; + expect( + menuRow.children.first.key, + const Key('assistant-gateway-provider-menu-badge'), + ); await tester.pumpWidget(const SizedBox.shrink()); await tester.pump(); diff --git a/test/features/assistant/assistant_page_composer_golden_test.dart b/test/features/assistant/assistant_page_composer_golden_test.dart index 5744323b..7b34e41a 100644 --- a/test/features/assistant/assistant_page_composer_golden_test.dart +++ b/test/features/assistant/assistant_page_composer_golden_test.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:xworkmate/app/app_controller_desktop_core.dart'; +import 'package:xworkmate/app/app_controller_desktop_workspace_execution.dart'; import 'package:xworkmate/features/assistant/assistant_page_composer_bar.dart'; import 'package:xworkmate/features/assistant/assistant_page_composer_clipboard.dart'; import 'package:xworkmate/features/assistant/assistant_page_composer_skill_models.dart'; @@ -110,6 +111,108 @@ void main() { ), ); }); + + testWidgets('renders composer with gateway provider lobster badge', ( + tester, + ) async { + await tester.binding.setSurfaceSize(const Size(1400, 320)); + addTearDown(() async => tester.binding.setSurfaceSize(null)); + + final root = Directory.systemTemp.createTempSync( + 'xworkmate-composer-golden-gateway-', + ); + final store = SecureConfigStore( + enableSecureStorage: false, + appDataRootPathResolver: () async => '${root.path}/settings.sqlite3', + secretRootPathResolver: () async => root.path, + supportRootPathResolver: () async => root.path, + ); + final controller = AppController( + store: store, + desktopPlatformService: UnsupportedDesktopPlatformService(), + skillDirectoryAccessService: _GoldenSkillDirectoryAccessService( + root.path, + ), + goTaskServiceClient: const _GoldenGoTaskServiceClient(), + singleAgentSharedSkillScanRootOverrides: const [], + ); + _seedBridgeProviders(controller, const [ + SingleAgentProvider.codex, + ]); + final inputController = TextEditingController(text: 'θ―·ζ•΄η†δ»Šε€©ηš„δ»»εŠ‘θΏ›ε±•'); + final focusNode = FocusNode(); + + addTearDown(() async { + controller.dispose(); + inputController.dispose(); + focusNode.dispose(); + if (root.existsSync()) { + await root.delete(recursive: true); + } + }); + + controller.appUiStateInternal = controller.appUiState.copyWith( + savedGatewayTargets: const ['gateway'], + ); + controller.lastObservedSettingsSnapshotInternal = + controller.settingsController.snapshotInternal; + controller.initializeAssistantThreadContext( + controller.currentSessionKey, + executionTarget: AssistantExecutionTarget.gateway, + ); + + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.light(platform: TargetPlatform.macOS), + home: Scaffold( + body: Center( + child: RepaintBoundary( + key: const ValueKey('assistant-gateway-composer-boundary'), + child: SizedBox( + width: 1280, + child: ComposerBarInternal( + controller: controller, + inputController: inputController, + focusNode: focusNode, + thinkingLabel: 'Normal', + showModelControl: false, + modelLabel: '', + modelOptions: const [], + attachments: const [], + availableSkills: const [], + selectedSkillKeys: const [], + onRemoveAttachment: (_) {}, + onToggleSkill: (_) {}, + onThinkingChanged: (_) {}, + onModelChanged: (_) async {}, + onOpenGateway: () {}, + onOpenAiGatewaySettings: () {}, + onReconnectGateway: () async {}, + onPickAttachments: () {}, + onAddAttachment: (_) {}, + onPasteImageAttachment: () async => null, + onContentHeightChanged: (_) {}, + onInputHeightChanged: (_) {}, + onSend: () async {}, + ), + ), + ), + ), + ), + ), + ); + await tester.pump(const Duration(milliseconds: 300)); + + expect( + find.byKey(const Key('assistant-gateway-provider-badge')), + findsOneWidget, + ); + + await expectLater( + find.byKey(const ValueKey('assistant-gateway-composer-boundary')), + matchesGoldenFile('goldens/assistant_page_composer_gateway_provider.png'), + ); + }); } void _seedBridgeProviders( diff --git a/test/features/assistant/goldens/assistant_page_composer_gateway_provider.png b/test/features/assistant/goldens/assistant_page_composer_gateway_provider.png new file mode 100644 index 00000000..051c9a24 Binary files /dev/null and b/test/features/assistant/goldens/assistant_page_composer_gateway_provider.png differ