Fix assistant provider selector and submit action
This commit is contained in:
parent
1171f12937
commit
7934b1b1d9
@ -567,6 +567,14 @@ class AppController extends ChangeNotifier {
|
||||
List<SingleAgentProvider> get bridgeProviderCatalog =>
|
||||
normalizeSingleAgentProviderList(bridgeProviderCatalogInternal);
|
||||
|
||||
List<SingleAgentProvider> get assistantProviderCatalog {
|
||||
final catalog = bridgeProviderCatalog;
|
||||
if (catalog.isNotEmpty) {
|
||||
return catalog;
|
||||
}
|
||||
return kPresetExternalAcpProviders;
|
||||
}
|
||||
|
||||
SingleAgentProvider? bridgeProviderForId(String providerId) {
|
||||
final normalizedProviderId = normalizeSingleAgentProviderId(providerId);
|
||||
if (normalizedProviderId.isEmpty) {
|
||||
@ -580,6 +588,30 @@ class AppController extends ChangeNotifier {
|
||||
return null;
|
||||
}
|
||||
|
||||
SingleAgentProvider resolveAssistantProvider(String? providerId) {
|
||||
final normalizedProviderId = normalizeSingleAgentProviderId(providerId ?? '');
|
||||
final catalog = assistantProviderCatalog;
|
||||
if (normalizedProviderId.isNotEmpty) {
|
||||
for (final provider in catalog) {
|
||||
if (provider.providerId == normalizedProviderId) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (catalog.isNotEmpty) {
|
||||
return catalog.first;
|
||||
}
|
||||
return SingleAgentProvider.unspecified;
|
||||
}
|
||||
|
||||
SingleAgentProvider assistantProviderForSession(String sessionKey) {
|
||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionKey,
|
||||
);
|
||||
final thread = taskThreadForSessionInternal(normalizedSessionKey);
|
||||
return resolveAssistantProvider(thread?.executionBinding.providerId);
|
||||
}
|
||||
|
||||
List<AssistantExecutionTarget> visibleAssistantExecutionTargets(
|
||||
Iterable<AssistantExecutionTarget> supportedTargets,
|
||||
) => const <AssistantExecutionTarget>[AssistantExecutionTarget.gateway];
|
||||
|
||||
@ -64,7 +64,12 @@ extension AppControllerDesktopExternalAcpRouting on AppController {
|
||||
.where((item) => item.trim().isNotEmpty)
|
||||
.toList(growable: false);
|
||||
|
||||
const resolvedExplicitProviderId = '';
|
||||
final resolvedProvider = assistantProviderForSession(normalizedSessionKey);
|
||||
final resolvedExplicitProviderId =
|
||||
thread?.hasExplicitProviderSelection == true &&
|
||||
!resolvedProvider.isUnspecified
|
||||
? resolvedProvider.providerId
|
||||
: '';
|
||||
final resolvedExplicitModel = thread?.hasExplicitModelSelection ?? false
|
||||
? assistantModelForSession(normalizedSessionKey)
|
||||
: '';
|
||||
|
||||
@ -290,8 +290,18 @@ extension AppControllerDesktopSkillPermissions on AppController {
|
||||
'TaskThread $normalizedSessionKey is missing a complete workspaceBinding.',
|
||||
);
|
||||
}
|
||||
final nextProvider = SingleAgentProvider.unspecified;
|
||||
const nextProviderSource = ThreadSelectionSource.inherited;
|
||||
final requestedProvider = singleAgentProvider?.isUnspecified == false
|
||||
? singleAgentProvider
|
||||
: null;
|
||||
final nextProvider = resolveAssistantProvider(
|
||||
requestedProvider?.providerId ??
|
||||
existing?.executionBinding.providerId ??
|
||||
existing?.contextState.latestResolvedProviderId,
|
||||
);
|
||||
final nextProviderSource =
|
||||
singleAgentProviderSource ??
|
||||
existing?.executionBinding.providerSource ??
|
||||
ThreadSelectionSource.inherited;
|
||||
final nextExecutionBinding =
|
||||
(executionBinding ??
|
||||
existing?.executionBinding ??
|
||||
|
||||
@ -301,6 +301,7 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
sessionId: sessionKey,
|
||||
threadId: sessionKey,
|
||||
target: currentTarget,
|
||||
provider: assistantProviderForSession(sessionKey),
|
||||
prompt: message,
|
||||
workingDirectory: workingDirectory,
|
||||
model: assistantModelForSession(sessionKey),
|
||||
|
||||
@ -219,19 +219,22 @@ extension AppControllerDesktopThreadBinding on AppController {
|
||||
required AssistantExecutionTarget executionTarget,
|
||||
ExecutionBinding? existingBinding,
|
||||
}) {
|
||||
const selectedProviderId = kCanonicalGatewayProviderId;
|
||||
final selectedProvider = resolveAssistantProvider(
|
||||
existingBinding?.providerId,
|
||||
);
|
||||
return (existingBinding ??
|
||||
ExecutionBinding(
|
||||
executionMode: ThreadExecutionMode.gateway,
|
||||
executorId: selectedProviderId,
|
||||
providerId: selectedProviderId,
|
||||
executorId: selectedProvider.providerId,
|
||||
providerId: selectedProvider.providerId,
|
||||
endpointId: '',
|
||||
))
|
||||
.copyWith(
|
||||
executionMode: ThreadExecutionMode.gateway,
|
||||
executorId: selectedProviderId,
|
||||
providerId: selectedProviderId,
|
||||
providerSource: ThreadSelectionSource.inherited,
|
||||
executorId: selectedProvider.providerId,
|
||||
providerId: selectedProvider.providerId,
|
||||
providerSource:
|
||||
existingBinding?.providerSource ?? ThreadSelectionSource.inherited,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -101,6 +101,43 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
|
||||
notifyIfActiveInternal();
|
||||
}
|
||||
|
||||
Future<void> setAssistantSingleAgentProvider(
|
||||
SingleAgentProvider provider,
|
||||
) async {
|
||||
final resolvedProvider = resolveAssistantProvider(provider.providerId);
|
||||
final sessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionsControllerInternal.currentSessionKey,
|
||||
);
|
||||
if (sessionKey.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final existing = taskThreadForSessionInternal(sessionKey);
|
||||
if (existing != null &&
|
||||
normalizeSingleAgentProviderId(existing.executionBinding.providerId) ==
|
||||
resolvedProvider.providerId &&
|
||||
existing.executionBinding.providerSource ==
|
||||
ThreadSelectionSource.explicit) {
|
||||
return;
|
||||
}
|
||||
if (!assistantThreadRecordsInternal.containsKey(sessionKey)) {
|
||||
initializeAssistantThreadContext(
|
||||
sessionKey,
|
||||
executionTarget: assistantExecutionTargetForSession(sessionKey),
|
||||
messageViewMode: assistantMessageViewModeForSession(sessionKey),
|
||||
);
|
||||
}
|
||||
upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
singleAgentProvider: resolvedProvider,
|
||||
singleAgentProviderSource: ThreadSelectionSource.explicit,
|
||||
latestResolvedProviderId: '',
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
await flushAssistantThreadPersistenceInternal();
|
||||
recomputeTasksInternal();
|
||||
notifyIfActiveInternal();
|
||||
}
|
||||
|
||||
Future<void> setAssistantMessageViewMode(
|
||||
AssistantMessageViewMode mode,
|
||||
) async {
|
||||
|
||||
@ -53,9 +53,6 @@ class ComposerBarInternal extends StatefulWidget {
|
||||
required this.onToggleSkill,
|
||||
required this.onThinkingChanged,
|
||||
required this.onModelChanged,
|
||||
required this.onOpenGateway,
|
||||
required this.onOpenAiGatewaySettings,
|
||||
required this.onReconnectGateway,
|
||||
required this.onPickAttachments,
|
||||
required this.onAddAttachment,
|
||||
required this.onPasteImageAttachment,
|
||||
@ -78,9 +75,6 @@ class ComposerBarInternal extends StatefulWidget {
|
||||
final ValueChanged<String> onToggleSkill;
|
||||
final ValueChanged<String> onThinkingChanged;
|
||||
final Future<void> Function(String modelId) onModelChanged;
|
||||
final VoidCallback onOpenGateway;
|
||||
final VoidCallback onOpenAiGatewaySettings;
|
||||
final Future<void> Function() onReconnectGateway;
|
||||
final VoidCallback onPickAttachments;
|
||||
final ValueChanged<ComposerAttachmentInternal> onAddAttachment;
|
||||
final AssistantClipboardImageReader onPasteImageAttachment;
|
||||
@ -316,10 +310,6 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
|
||||
final uiFeatures = controller.featuresFor(
|
||||
resolveUiFeaturePlatformFromContext(context),
|
||||
);
|
||||
final connectionState = controller.currentAssistantConnectionState;
|
||||
final connected = connectionState.connected;
|
||||
final reconnectAvailable = controller.canQuickConnectGateway;
|
||||
final connecting = connectionState.connecting;
|
||||
final visibleExecutionTargets = controller.visibleAssistantExecutionTargets(
|
||||
uiFeatures.availableExecutionTargets,
|
||||
);
|
||||
@ -337,16 +327,14 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
|
||||
executionTarget,
|
||||
);
|
||||
final permissionLevel = controller.assistantPermissionLevel;
|
||||
final availableProviders = controller.assistantProviderCatalog;
|
||||
final selectedProvider = controller.assistantProviderForSession(
|
||||
controller.currentSessionKey,
|
||||
);
|
||||
final selectedSkills = widget.availableSkills
|
||||
.where((skill) => widget.selectedSkillKeys.contains(skill.key))
|
||||
.toList(growable: false);
|
||||
final submitLabel = connected
|
||||
? appText('提交', 'Submit')
|
||||
: connecting
|
||||
? appText('连接中…', 'Connecting…')
|
||||
: reconnectAvailable
|
||||
? appText('重连', 'Reconnect')
|
||||
: appText('连接', 'Connect');
|
||||
final submitLabel = appText('提交', 'Submit');
|
||||
|
||||
reportContentHeightInternal();
|
||||
|
||||
@ -432,32 +420,48 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
if (!connecting) ...[
|
||||
if (availableProviders.isNotEmpty) ...[
|
||||
PopupMenuButton<String>(
|
||||
key: const Key('assistant-gateway-provider-button'),
|
||||
tooltip: appText('Gateway Provider', 'Gateway Provider'),
|
||||
onSelected: (_) {},
|
||||
itemBuilder: (context) => const <PopupMenuEntry<String>>[
|
||||
PopupMenuItem<String>(
|
||||
value: kCanonicalGatewayProviderId,
|
||||
key: Key('assistant-gateway-provider-menu-item-openclaw'),
|
||||
child: Row(
|
||||
children: [
|
||||
GatewayProviderBadgeInternal(
|
||||
key: Key('assistant-gateway-provider-menu-badge'),
|
||||
key: const Key('assistant-provider-button'),
|
||||
tooltip: appText('智能体 Provider', 'Agent Provider'),
|
||||
onSelected: (providerId) async {
|
||||
await controller.setAssistantSingleAgentProvider(
|
||||
controller.resolveAssistantProvider(providerId),
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => availableProviders
|
||||
.map(
|
||||
(provider) => PopupMenuItem<String>(
|
||||
value: provider.providerId,
|
||||
key: Key(
|
||||
'assistant-provider-menu-item-${provider.providerId}',
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Expanded(child: Text(kCanonicalGatewayProviderLabel)),
|
||||
Icon(Icons.check_rounded, size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Row(
|
||||
children: [
|
||||
SingleAgentProviderBadgeInternal(
|
||||
key: Key(
|
||||
'assistant-provider-menu-badge-${provider.providerId}',
|
||||
),
|
||||
provider: provider,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Text(provider.label)),
|
||||
if (provider == selectedProvider)
|
||||
const Icon(Icons.check_rounded, size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(growable: false),
|
||||
child: ComposerToolbarChipInternal(
|
||||
leading: const GatewayProviderBadgeInternal(
|
||||
key: Key('assistant-gateway-provider-badge'),
|
||||
leading: SingleAgentProviderBadgeInternal(
|
||||
key: const Key('assistant-provider-badge'),
|
||||
provider: selectedProvider,
|
||||
),
|
||||
tooltip: gatewayProviderTooltipInternal(),
|
||||
tooltip: providerTooltipInternal(selectedProvider),
|
||||
showChevron: true,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
@ -707,15 +711,7 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
|
||||
message: submitLabel,
|
||||
child: FilledButton(
|
||||
key: const Key('assistant-send-button'),
|
||||
onPressed: connecting
|
||||
? null
|
||||
: connected
|
||||
? widget.onSend
|
||||
: reconnectAvailable
|
||||
? () async {
|
||||
await widget.onReconnectGateway();
|
||||
}
|
||||
: widget.onOpenGateway,
|
||||
onPressed: widget.onSend,
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
@ -729,14 +725,7 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
connected
|
||||
? Icons.arrow_upward_rounded
|
||||
: reconnectAvailable
|
||||
? Icons.refresh_rounded
|
||||
: Icons.link_rounded,
|
||||
size: 18,
|
||||
),
|
||||
const Icon(Icons.arrow_upward_rounded, size: 18),
|
||||
const SizedBox(width: 4),
|
||||
Text(submitLabel),
|
||||
],
|
||||
|
||||
@ -245,35 +245,3 @@ 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -527,9 +527,6 @@ class AssistantLowerPaneInternal extends StatelessWidget {
|
||||
required this.onToggleSkill,
|
||||
required this.onThinkingChanged,
|
||||
required this.onModelChanged,
|
||||
required this.onOpenGateway,
|
||||
required this.onOpenAiGatewaySettings,
|
||||
required this.onReconnectGateway,
|
||||
required this.onPickAttachments,
|
||||
required this.onAddAttachment,
|
||||
required this.onPasteImageAttachment,
|
||||
@ -553,9 +550,6 @@ class AssistantLowerPaneInternal extends StatelessWidget {
|
||||
final ValueChanged<String> onToggleSkill;
|
||||
final ValueChanged<String> onThinkingChanged;
|
||||
final Future<void> Function(String modelId) onModelChanged;
|
||||
final VoidCallback onOpenGateway;
|
||||
final VoidCallback onOpenAiGatewaySettings;
|
||||
final Future<void> Function() onReconnectGateway;
|
||||
final VoidCallback onPickAttachments;
|
||||
final ValueChanged<ComposerAttachmentInternal> onAddAttachment;
|
||||
final AssistantClipboardImageReader onPasteImageAttachment;
|
||||
@ -587,9 +581,6 @@ class AssistantLowerPaneInternal extends StatelessWidget {
|
||||
onToggleSkill: onToggleSkill,
|
||||
onThinkingChanged: onThinkingChanged,
|
||||
onModelChanged: onModelChanged,
|
||||
onOpenGateway: onOpenGateway,
|
||||
onOpenAiGatewaySettings: onOpenAiGatewaySettings,
|
||||
onReconnectGateway: onReconnectGateway,
|
||||
onPickAttachments: onPickAttachments,
|
||||
onAddAttachment: onAddAttachment,
|
||||
onPasteImageAttachment: onPasteImageAttachment,
|
||||
|
||||
@ -219,15 +219,6 @@ extension AssistantPageStateClosureInternal on AssistantPageStateInternal {
|
||||
controller.currentSessionKey,
|
||||
modelId,
|
||||
),
|
||||
onOpenGateway: AssistantPageStateActionsInternal(
|
||||
this,
|
||||
).openGatewaySettingsInternal,
|
||||
onOpenAiGatewaySettings: AssistantPageStateActionsInternal(
|
||||
this,
|
||||
).openAiGatewaySettingsInternal,
|
||||
onReconnectGateway: AssistantPageStateActionsInternal(
|
||||
this,
|
||||
).connectFromSavedSettingsOrShowDialogInternal,
|
||||
onPickAttachments: AssistantPageStateActionsInternal(
|
||||
this,
|
||||
).pickAttachmentsInternal,
|
||||
|
||||
@ -43,9 +43,9 @@ String executionTargetTooltipInternal(AssistantExecutionTarget target) =>
|
||||
'Task dialog mode: ${target.compactLabel}',
|
||||
);
|
||||
|
||||
String gatewayProviderTooltipInternal() => appText(
|
||||
'Gateway Provider: 🦞 $kCanonicalGatewayProviderLabel',
|
||||
'Gateway Provider: 🦞 $kCanonicalGatewayProviderLabel',
|
||||
String providerTooltipInternal(SingleAgentProvider provider) => appText(
|
||||
'智能体 Provider: ${provider.label}',
|
||||
'Agent provider: ${provider.label}',
|
||||
);
|
||||
|
||||
String modelTooltipInternal(String modelLabel) =>
|
||||
|
||||
129
test/features/assistant/assistant_lower_pane_test.dart
Normal file
129
test/features/assistant/assistant_lower_pane_test.dart
Normal file
@ -0,0 +1,129 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/app/app_controller.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_composer_clipboard.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_composer_skill_models.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_main.dart';
|
||||
import 'package:xworkmate/theme/app_theme.dart';
|
||||
import 'package:xworkmate/widgets/surface_card.dart';
|
||||
|
||||
void main() {
|
||||
group('AssistantLowerPaneInternal', () {
|
||||
testWidgets('shows assistant providers and allows switching provider', (
|
||||
tester,
|
||||
) async {
|
||||
final controller = AppController();
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await controller.sessionsController.switchSession('session-1');
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
child: _buildLowerPane(controller: controller),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(const Key('assistant-provider-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byKey(const Key('assistant-provider-menu-item-codex')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-provider-menu-item-opencode')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-provider-menu-item-gemini')),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.tap(
|
||||
find.byKey(const Key('assistant-provider-menu-item-opencode')),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
controller.assistantProviderForSession(controller.currentSessionKey)
|
||||
.providerId,
|
||||
'opencode',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('uses submit button instead of connect action', (tester) async {
|
||||
final controller = AppController();
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await controller.sessionsController.switchSession('session-1');
|
||||
|
||||
var sendCount = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
child: _buildLowerPane(
|
||||
controller: controller,
|
||||
inputController: TextEditingController(text: 'hello'),
|
||||
onSend: () async {
|
||||
sendCount += 1;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('提交'), findsOneWidget);
|
||||
expect(find.text('连接'), findsNothing);
|
||||
|
||||
await tester.tap(find.byKey(const Key('assistant-send-button')));
|
||||
await tester.pump();
|
||||
|
||||
expect(sendCount, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildTestApp({required Widget child}) {
|
||||
return MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: SizedBox(width: 1400, height: 360, child: child),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLowerPane({
|
||||
required AppController controller,
|
||||
TextEditingController? inputController,
|
||||
Future<void> Function()? onSend,
|
||||
}) {
|
||||
final composerController = inputController ?? TextEditingController();
|
||||
return SurfaceCard(
|
||||
child: AssistantLowerPaneInternal(
|
||||
bottomContentInset: 0,
|
||||
controller: controller,
|
||||
inputController: composerController,
|
||||
focusNode: FocusNode(),
|
||||
thinkingLabel: 'medium',
|
||||
showModelControl: false,
|
||||
modelLabel: 'gpt-5.4',
|
||||
modelOptions: const <String>[],
|
||||
attachments: const <ComposerAttachmentInternal>[],
|
||||
availableSkills: const <ComposerSkillOptionInternal>[],
|
||||
selectedSkillKeys: const <String>[],
|
||||
onRemoveAttachment: (_) {},
|
||||
onToggleSkill: (_) {},
|
||||
onThinkingChanged: (_) {},
|
||||
onModelChanged: (_) async {},
|
||||
onPickAttachments: () {},
|
||||
onAddAttachment: (_) {},
|
||||
onPasteImageAttachment: () async => null,
|
||||
onComposerContentHeightChanged: (_) {},
|
||||
onComposerInputHeightChanged: (_) {},
|
||||
onSend: onSend ?? () async {},
|
||||
),
|
||||
);
|
||||
}
|
||||
66
test/golden/assistant_lower_pane_golden_test.dart
Normal file
66
test/golden/assistant_lower_pane_golden_test.dart
Normal file
@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/app/app_controller.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_composer_clipboard.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_composer_skill_models.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_main.dart';
|
||||
import 'package:xworkmate/theme/app_theme.dart';
|
||||
import 'package:xworkmate/widgets/surface_card.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('assistant lower pane matches desktop baseline', (tester) async {
|
||||
final controller = AppController();
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await controller.sessionsController.switchSession('session-1');
|
||||
await tester.binding.setSurfaceSize(const Size(1400, 360));
|
||||
addTearDown(() async {
|
||||
await tester.binding.setSurfaceSize(null);
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 1400,
|
||||
height: 360,
|
||||
child: SurfaceCard(
|
||||
child: AssistantLowerPaneInternal(
|
||||
bottomContentInset: 0,
|
||||
controller: controller,
|
||||
inputController: TextEditingController(text: '修复智能体模式'),
|
||||
focusNode: FocusNode(),
|
||||
thinkingLabel: 'medium',
|
||||
showModelControl: false,
|
||||
modelLabel: 'gpt-5.4',
|
||||
modelOptions: const <String>[],
|
||||
attachments: const <ComposerAttachmentInternal>[],
|
||||
availableSkills: const <ComposerSkillOptionInternal>[],
|
||||
selectedSkillKeys: const <String>[],
|
||||
onRemoveAttachment: (_) {},
|
||||
onToggleSkill: (_) {},
|
||||
onThinkingChanged: (_) {},
|
||||
onModelChanged: (_) async {},
|
||||
onPickAttachments: () {},
|
||||
onAddAttachment: (_) {},
|
||||
onPasteImageAttachment: () async => null,
|
||||
onComposerContentHeightChanged: (_) {},
|
||||
onComposerInputHeightChanged: (_) {},
|
||||
onSend: () async {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await expectLater(
|
||||
find.byType(MaterialApp),
|
||||
matchesGoldenFile('goldens/assistant_lower_pane.png'),
|
||||
);
|
||||
});
|
||||
}
|
||||
BIN
test/golden/goldens/assistant_lower_pane.png
Normal file
BIN
test/golden/goldens/assistant_lower_pane.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
Loading…
Reference in New Issue
Block a user