From 7625988cfadd21176ef9d86a4d2386ba24abc4a2 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sat, 23 May 2026 11:43:09 +0800 Subject: [PATCH] fix: group bridge skills by source --- ..._controller_desktop_skill_permissions.dart | 51 +++------ ...app_controller_desktop_thread_storage.dart | 5 +- .../assistant_page_composer_skill_models.dart | 49 ++++++++ .../assistant_page_composer_skill_picker.dart | 96 ++++++++++++++-- .../runtime_models_runtime_payloads.dart | 92 --------------- .../assistant/assistant_lower_pane_test.dart | 108 ++++++++++++++++++ .../assistant_execution_target_test.dart | 93 +++++++++++++-- 7 files changed, 340 insertions(+), 154 deletions(-) diff --git a/lib/app/app_controller_desktop_skill_permissions.dart b/lib/app/app_controller_desktop_skill_permissions.dart index f7e138ca..221d9de7 100644 --- a/lib/app/app_controller_desktop_skill_permissions.dart +++ b/lib/app/app_controller_desktop_skill_permissions.dart @@ -45,32 +45,6 @@ import 'app_controller_desktop_runtime_helpers.dart'; // ignore_for_file: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member extension AppControllerDesktopSkillPermissions on AppController { - Future replaceSingleAgentThreadSkillsInternal( - String sessionKey, - List importedSkills, - ) async { - final normalizedSessionKey = normalizedAssistantSessionKeyInternal( - sessionKey, - ); - final importedKeys = importedSkills.map((item) => item.key).toSet(); - final nextSelected = - (assistantThreadRecordsInternal[normalizedSessionKey] - ?.selectedSkillKeys ?? - const []) - .where(importedKeys.contains) - .toList(growable: false); - upsertTaskThreadInternal( - normalizedSessionKey, - importedSkills: importedSkills, - selectedSkillKeys: nextSelected, - selectedSkillsSource: assistantThreadRecordsInternal[normalizedSessionKey] - ?.contextState - .selectedSkillsSource, - updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), - ); - notifyIfActiveInternal(); - } - void upsertTaskThreadInternal( String sessionKey, { ThreadOwnerScope? ownerScope, @@ -84,7 +58,6 @@ extension AppControllerDesktopSkillPermissions on AppController { bool? archived, AssistantExecutionTarget? executionTarget, AssistantMessageViewMode? messageViewMode, - List? importedSkills, List? selectedSkillKeys, String? assistantModelId, SingleAgentProvider? selectedProvider, @@ -120,15 +93,19 @@ extension AppControllerDesktopSkillPermissions on AppController { ThreadExecutionMode.gateway => AssistantExecutionTarget.gateway, null => AssistantExecutionTarget.agent, }; - final nextImportedSkills = - importedSkills ?? - existing?.importedSkills ?? - const []; - final importedKeys = nextImportedSkills.map((item) => item.key).toSet(); - final nextSelectedSkillKeys = - (selectedSkillKeys ?? existing?.selectedSkillKeys ?? const []) - .where(importedKeys.contains) - .toList(growable: false); + final bridgeSkillKeys = skills + .map((item) => item.skillKey.trim()) + .where((item) => item.isNotEmpty) + .toSet(); + final selectedSkillCandidates = + selectedSkillKeys ?? existing?.selectedSkillKeys ?? const []; + final nextSelectedSkillKeys = selectedSkillCandidates + .map((item) => item.trim()) + .where((item) => item.isNotEmpty) + .where( + (item) => bridgeSkillKeys.isEmpty || bridgeSkillKeys.contains(item), + ) + .toList(growable: false); final nextMessages = messages ?? existing?.messages ?? @@ -213,7 +190,6 @@ extension AppControllerDesktopSkillPermissions on AppController { nextExecutionTarget, ), selectedSkillKeys: const [], - importedSkills: const [], permissionLevel: AssistantPermissionLevel.defaultAccess, messageViewMode: AssistantMessageViewMode.rendered, latestResolvedRuntimeModel: '', @@ -230,7 +206,6 @@ extension AppControllerDesktopSkillPermissions on AppController { .copyWith( messages: nextMessages, messageViewMode: messageViewMode, - importedSkills: nextImportedSkills, selectedSkillKeys: nextSelectedSkillKeys, selectedModelId: assistantModelId ?? diff --git a/lib/app/app_controller_desktop_thread_storage.dart b/lib/app/app_controller_desktop_thread_storage.dart index 6b27e322..a4417761 100644 --- a/lib/app/app_controller_desktop_thread_storage.dart +++ b/lib/app/app_controller_desktop_thread_storage.dart @@ -532,9 +532,8 @@ extension AppControllerDesktopThreadStorage on AppController { archived: record.archived, messageViewMode: record.messageViewMode, selectedSkillKeys: record.selectedSkillKeys - .where( - (item) => record.importedSkills.any((skill) => skill.key == item), - ) + .map((item) => item.trim()) + .where((item) => item.isNotEmpty) .toList(growable: false), assistantModelId: record.assistantModelId.trim().isEmpty ? resolvedAssistantModelForTargetInternal(normalizedExecutionTarget) diff --git a/lib/features/assistant/assistant_page_composer_skill_models.dart b/lib/features/assistant/assistant_page_composer_skill_models.dart index 41787f90..49d12623 100644 --- a/lib/features/assistant/assistant_page_composer_skill_models.dart +++ b/lib/features/assistant/assistant_page_composer_skill_models.dart @@ -45,6 +45,7 @@ ComposerSkillOptionInternal skillOptionFromGatewayInternal( : skill.skillKey.trim(); final label = skill.name.trim().isEmpty ? key : skill.name.trim(); final sourceLabel = skill.source.trim().isEmpty ? 'Gateway' : skill.source; + final group = skillGroupForSourceInternal(skill.source); final description = skill.description.trim().isEmpty ? appText('可在当前任务中调用的技能。', 'Skill available in the current task.') : skill.description.trim(); @@ -54,16 +55,62 @@ ComposerSkillOptionInternal skillOptionFromGatewayInternal( label: label, description: description, sourceLabel: sourceLabel, + groupLabel: group.label, + groupSortOrder: group.sortOrder, icon: Icons.key_rounded, ); } +ComposerSkillGroupInternal skillGroupForSourceInternal(String source) { + final normalized = source.trim().toLowerCase(); + if (normalized == 'openclaw-workspace') { + return const ComposerSkillGroupInternal( + label: 'Workspace Skills', + sortOrder: 0, + ); + } + if (normalized.startsWith('agents-skills-') || + normalized == 'agent' || + normalized.startsWith('agent-') || + normalized.contains('personal')) { + return const ComposerSkillGroupInternal( + label: 'Agent Skills', + sortOrder: 1, + ); + } + if (normalized == 'bridge' || normalized == 'gateway') { + return const ComposerSkillGroupInternal( + label: 'Gateway Skills', + sortOrder: 2, + ); + } + if (normalized.isEmpty) { + return const ComposerSkillGroupInternal( + label: 'Gateway Skills', + sortOrder: 2, + ); + } + return const ComposerSkillGroupInternal(label: 'Other Skills', sortOrder: 3); +} + +class ComposerSkillGroupInternal { + const ComposerSkillGroupInternal({ + required this.label, + required this.sortOrder, + }); + + final String label; + final int sortOrder; +} + class ComposerSkillOptionInternal { const ComposerSkillOptionInternal({ required this.key, required this.label, required this.description, required this.sourceLabel, + required this.groupLabel, + required this.groupSortOrder, required this.icon, }); @@ -71,5 +118,7 @@ class ComposerSkillOptionInternal { final String label; final String description; final String sourceLabel; + final String groupLabel; + final int groupSortOrder; final IconData icon; } diff --git a/lib/features/assistant/assistant_page_composer_skill_picker.dart b/lib/features/assistant/assistant_page_composer_skill_picker.dart index 8788a0f1..74ae5af4 100644 --- a/lib/features/assistant/assistant_page_composer_skill_picker.dart +++ b/lib/features/assistant/assistant_page_composer_skill_picker.dart @@ -94,6 +94,7 @@ class SkillPickerPopoverInternal extends StatelessWidget { Widget build(BuildContext context) { final palette = context.palette; final theme = Theme.of(context); + final groupedSkills = skillPickerSectionsInternal(filteredSkills); return Material( key: const Key('assistant-skill-picker-popover'), color: Colors.transparent, @@ -174,19 +175,32 @@ class SkillPickerPopoverInternal extends StatelessWidget { ), ), ) - : ListView.separated( + : ListView.builder( padding: const EdgeInsets.fromLTRB(12, 12, 12, 12), - itemCount: filteredSkills.length, - separatorBuilder: (_, _) => const SizedBox(height: 8), + itemCount: groupedSkills.length, itemBuilder: (context, index) { - final skill = filteredSkills[index]; - return SkillPickerTileInternal( - key: ValueKey( - 'assistant-skill-option-${skill.key}', + final row = groupedSkills[index]; + if (row.headerLabel != null) { + return SkillPickerGroupHeaderInternal( + key: ValueKey( + 'assistant-skill-group-${row.headerLabel}', + ), + label: row.headerLabel!, + ); + } + final skill = row.skill!; + return Padding( + padding: EdgeInsets.only( + bottom: index == groupedSkills.length - 1 ? 0 : 8, + ), + child: SkillPickerTileInternal( + key: ValueKey( + 'assistant-skill-option-${skill.key}', + ), + option: skill, + selected: selectedSkillKeys.contains(skill.key), + onTap: () => onToggleSkill(skill.key), ), - option: skill, - selected: selectedSkillKeys.contains(skill.key), - onTap: () => onToggleSkill(skill.key), ); }, ), @@ -199,6 +213,68 @@ class SkillPickerPopoverInternal extends StatelessWidget { } } +List skillPickerSectionsInternal( + List skills, +) { + final groupsByLabel = >{}; + final groupSortOrders = {}; + for (final skill in skills) { + groupsByLabel.putIfAbsent(skill.groupLabel, () => []).add(skill); + groupSortOrders[skill.groupLabel] = skill.groupSortOrder; + } + final labels = groupsByLabel.keys.toList(growable: false) + ..sort((a, b) { + final orderCompare = (groupSortOrders[a] ?? 999).compareTo( + groupSortOrders[b] ?? 999, + ); + if (orderCompare != 0) { + return orderCompare; + } + return a.compareTo(b); + }); + + final rows = []; + for (final label in labels) { + rows.add(SkillPickerRowInternal.header(label)); + for (final skill in groupsByLabel[label]!) { + rows.add(SkillPickerRowInternal.skill(skill)); + } + } + return rows; +} + +class SkillPickerRowInternal { + const SkillPickerRowInternal.header(this.headerLabel) : skill = null; + const SkillPickerRowInternal.skill(this.skill) : headerLabel = null; + + final String? headerLabel; + final ComposerSkillOptionInternal? skill; +} + +class SkillPickerGroupHeaderInternal extends StatelessWidget { + const SkillPickerGroupHeaderInternal({super.key, required this.label}); + + final String label; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final palette = context.palette; + return Padding( + padding: const EdgeInsets.fromLTRB(4, 6, 4, 8), + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.labelMedium?.copyWith( + color: palette.textSecondary, + fontWeight: FontWeight.w700, + ), + ), + ); + } +} + class SkillPickerTileInternal extends StatelessWidget { const SkillPickerTileInternal({ super.key, diff --git a/lib/runtime/runtime_models_runtime_payloads.dart b/lib/runtime/runtime_models_runtime_payloads.dart index 41314d26..7c3b2212 100644 --- a/lib/runtime/runtime_models_runtime_payloads.dart +++ b/lib/runtime/runtime_models_runtime_payloads.dart @@ -388,70 +388,6 @@ class GatewayChatMessage { } } -class AssistantThreadSkillEntry { - const AssistantThreadSkillEntry({ - required this.key, - required this.label, - required this.description, - this.source = '', - required this.sourcePath, - this.scope = '', - required this.sourceLabel, - }); - - final String key; - final String label; - final String description; - final String source; - final String sourcePath; - final String scope; - final String sourceLabel; - - AssistantThreadSkillEntry copyWith({ - String? key, - String? label, - String? description, - String? source, - String? sourcePath, - String? scope, - String? sourceLabel, - }) { - return AssistantThreadSkillEntry( - key: key ?? this.key, - label: label ?? this.label, - description: description ?? this.description, - source: source ?? this.source, - sourcePath: sourcePath ?? this.sourcePath, - scope: scope ?? this.scope, - sourceLabel: sourceLabel ?? this.sourceLabel, - ); - } - - Map toJson() { - return { - 'key': key, - 'label': label, - 'description': description, - 'source': source, - 'sourcePath': sourcePath, - 'scope': scope, - 'sourceLabel': sourceLabel, - }; - } - - factory AssistantThreadSkillEntry.fromJson(Map json) { - return AssistantThreadSkillEntry( - key: json['key']?.toString() ?? '', - label: json['label']?.toString() ?? '', - description: json['description']?.toString() ?? '', - source: json['source']?.toString() ?? '', - sourcePath: json['sourcePath']?.toString() ?? '', - scope: json['scope']?.toString() ?? '', - sourceLabel: json['sourceLabel']?.toString() ?? '', - ); - } -} - const int taskThreadSchemaVersion = 20260411; enum ThreadRealm { local, remote } @@ -729,7 +665,6 @@ class ThreadContextState { required this.messages, required this.selectedModelId, required this.selectedSkillKeys, - required this.importedSkills, required this.permissionLevel, required this.messageViewMode, required this.latestResolvedRuntimeModel, @@ -747,7 +682,6 @@ class ThreadContextState { final List messages; final String selectedModelId; final List selectedSkillKeys; - final List importedSkills; final AssistantPermissionLevel permissionLevel; final AssistantMessageViewMode messageViewMode; final String latestResolvedRuntimeModel; @@ -765,7 +699,6 @@ class ThreadContextState { List? messages, String? selectedModelId, List? selectedSkillKeys, - List? importedSkills, AssistantPermissionLevel? permissionLevel, AssistantMessageViewMode? messageViewMode, String? latestResolvedRuntimeModel, @@ -784,7 +717,6 @@ class ThreadContextState { messages: messages ?? this.messages, selectedModelId: selectedModelId ?? this.selectedModelId, selectedSkillKeys: selectedSkillKeys ?? this.selectedSkillKeys, - importedSkills: importedSkills ?? this.importedSkills, permissionLevel: permissionLevel ?? this.permissionLevel, messageViewMode: messageViewMode ?? this.messageViewMode, latestResolvedRuntimeModel: @@ -814,9 +746,6 @@ class ThreadContextState { 'messages': messages.map((item) => item.toJson()).toList(growable: false), 'selectedModelId': selectedModelId, 'selectedSkillKeys': selectedSkillKeys, - 'importedSkills': importedSkills - .map((item) => item.toJson()) - .toList(growable: false), 'permissionLevel': permissionLevel.name, 'messageViewMode': messageViewMode.name, 'latestResolvedRuntimeModel': latestResolvedRuntimeModel, @@ -850,18 +779,6 @@ class ThreadContextState { ) .toList(growable: false) : const []; - final rawImportedSkills = json['importedSkills']; - final importedSkills = rawImportedSkills is List - ? rawImportedSkills - .whereType() - .map( - (item) => AssistantThreadSkillEntry.fromJson( - item.cast(), - ), - ) - .where((item) => item.key.trim().isNotEmpty) - .toList(growable: false) - : const []; final rawSelectedSkillKeys = json['selectedSkillKeys']; final selectedSkillKeys = rawSelectedSkillKeys is List ? rawSelectedSkillKeys @@ -874,7 +791,6 @@ class ThreadContextState { messages: messages, selectedModelId: json['selectedModelId']?.toString() ?? '', selectedSkillKeys: selectedSkillKeys, - importedSkills: importedSkills, permissionLevel: AssistantPermissionLevelCopy.fromJsonValue( json['permissionLevel']?.toString(), ), @@ -1002,7 +918,6 @@ class TaskThread { List? messages, bool? archived, AssistantMessageViewMode? messageViewMode, - List? importedSkills, List? selectedSkillKeys, String? assistantModelId, String? gatewayEntryState, @@ -1041,8 +956,6 @@ class TaskThread { messages: messages ?? const [], selectedModelId: assistantModelId?.trim() ?? '', selectedSkillKeys: selectedSkillKeys ?? const [], - importedSkills: - importedSkills ?? const [], permissionLevel: permissionLevel ?? AssistantPermissionLevel.defaultAccess, messageViewMode: @@ -1090,8 +1003,6 @@ class TaskThread { String get sessionKey => threadId; List get messages => contextState.messages; - List get importedSkills => - contextState.importedSkills; List get selectedSkillKeys => contextState.selectedSkillKeys; String get assistantModelId => contextState.selectedModelId; AssistantMessageViewMode get messageViewMode => contextState.messageViewMode; @@ -1134,7 +1045,6 @@ class TaskThread { List? messages, bool? archived, AssistantMessageViewMode? messageViewMode, - List? importedSkills, List? selectedSkillKeys, String? assistantModelId, ThreadSelectionSource? assistantModelSource, @@ -1158,7 +1068,6 @@ class TaskThread { contextState: (contextState ?? this.contextState).copyWith( messages: messages, messageViewMode: messageViewMode, - importedSkills: importedSkills, selectedSkillKeys: selectedSkillKeys, selectedModelId: assistantModelId, selectedModelSource: assistantModelSource, @@ -1276,7 +1185,6 @@ class TaskThread { 'messages': json['messages'], 'selectedModelId': json['assistantModelId'], 'selectedSkillKeys': json['selectedSkillKeys'], - 'importedSkills': json['importedSkills'], 'permissionLevel': json['permissionLevel'], 'messageViewMode': json['messageViewMode'], 'latestResolvedRuntimeModel': json['latestResolvedRuntimeModel'], diff --git a/test/features/assistant/assistant_lower_pane_test.dart b/test/features/assistant/assistant_lower_pane_test.dart index 527a27bd..8df78c94 100644 --- a/test/features/assistant/assistant_lower_pane_test.dart +++ b/test/features/assistant/assistant_lower_pane_test.dart @@ -3,6 +3,7 @@ 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_composer_skill_picker.dart'; import 'package:xworkmate/features/assistant/assistant_page_main.dart'; import 'package:xworkmate/runtime/runtime_models.dart'; import 'package:xworkmate/theme/app_theme.dart'; @@ -396,6 +397,113 @@ void main() { expect(sendCount, 1); }); + + testWidgets('groups all visible skills by source', (tester) async { + final toggledKeys = []; + final searchController = TextEditingController(); + final focusNode = FocusNode(); + addTearDown(searchController.dispose); + addTearDown(focusNode.dispose); + + await tester.pumpWidget( + _buildTestApp( + child: SkillPickerPopoverInternal( + maxHeight: 360, + searchController: searchController, + searchFocusNode: focusNode, + selectedSkillKeys: const [], + filteredSkills: const [ + ComposerSkillOptionInternal( + key: 'pdf', + label: 'PDF', + description: 'Create PDF files', + sourceLabel: 'openclaw-workspace', + groupLabel: 'Workspace Skills', + groupSortOrder: 0, + icon: Icons.key_rounded, + ), + ComposerSkillOptionInternal( + key: 'browser-automation', + label: 'Browser Automation', + description: 'Automate browsers', + sourceLabel: 'agents-skills-personal', + groupLabel: 'Agent Skills', + groupSortOrder: 1, + icon: Icons.key_rounded, + ), + ComposerSkillOptionInternal( + key: 'gateway-search', + label: 'Gateway Search', + description: 'Search through the gateway', + sourceLabel: 'gateway', + groupLabel: 'Gateway Skills', + groupSortOrder: 2, + icon: Icons.key_rounded, + ), + ], + isLoading: false, + hasQuery: false, + onQueryChanged: (_) {}, + onToggleSkill: toggledKeys.add, + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Workspace Skills'), findsOneWidget); + expect(find.text('Agent Skills'), findsOneWidget); + expect(find.text('Gateway Skills'), findsOneWidget); + + await tester.tap( + find.byKey( + const ValueKey('assistant-skill-option-browser-automation'), + ), + ); + await tester.pump(); + + expect(toggledKeys, const ['browser-automation']); + }); + + testWidgets('search results only keep groups with matching skills', ( + tester, + ) async { + final searchController = TextEditingController(text: 'gateway'); + final focusNode = FocusNode(); + addTearDown(searchController.dispose); + addTearDown(focusNode.dispose); + + await tester.pumpWidget( + _buildTestApp( + child: SkillPickerPopoverInternal( + maxHeight: 360, + searchController: searchController, + searchFocusNode: focusNode, + selectedSkillKeys: const [], + filteredSkills: const [ + ComposerSkillOptionInternal( + key: 'gateway-search', + label: 'Gateway Search', + description: 'Search through the gateway', + sourceLabel: 'gateway', + groupLabel: 'Gateway Skills', + groupSortOrder: 2, + icon: Icons.key_rounded, + ), + ], + isLoading: false, + hasQuery: true, + onQueryChanged: (_) {}, + onToggleSkill: (_) {}, + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Gateway Skills'), findsOneWidget); + expect(find.text('Gateway Search'), findsOneWidget); + expect(find.text('Workspace Skills'), findsNothing); + expect(find.text('Agent Skills'), findsNothing); + }); }); } diff --git a/test/runtime/assistant_execution_target_test.dart b/test/runtime/assistant_execution_target_test.dart index 0b257639..929b1062 100644 --- a/test/runtime/assistant_execution_target_test.dart +++ b/test/runtime/assistant_execution_target_test.dart @@ -476,10 +476,90 @@ void main() { expect(option.key, 'browser-fetch'); expect(option.label, 'Browser Fetch'); expect(option.description, 'Bridge-managed browser skill'); + expect(option.groupLabel, 'Gateway Skills'); expect(option.icon, Icons.key_rounded); }, ); + test('all bridge skill sources remain selectable and routed', () async { + final fakeGoTaskService = _RecordingGoTaskServiceClient(); + final controller = _connectedGatewayController(fakeGoTaskService); + addTearDown(controller.dispose); + controller.skillsControllerInternal.itemsInternal = + const [ + GatewaySkillSummary( + name: 'Workspace PDF', + description: 'Write PDF documents', + source: 'openclaw-workspace', + skillKey: 'pdf', + primaryEnv: null, + eligible: true, + disabled: false, + missingBins: [], + missingEnv: [], + missingConfig: [], + ), + GatewaySkillSummary( + name: 'Browser Automation', + description: 'Use browser automation', + source: 'agents-skills-personal', + skillKey: 'browser-automation', + primaryEnv: null, + eligible: true, + disabled: false, + missingBins: [], + missingEnv: [], + missingConfig: [], + ), + GatewaySkillSummary( + name: 'Gateway Search', + description: 'Search through the gateway', + source: 'gateway', + skillKey: 'gateway-search', + primaryEnv: null, + eligible: true, + disabled: false, + missingBins: [], + missingEnv: [], + missingConfig: [], + ), + ]; + await _selectGatewaySession(controller, 'unit-skill-source-groups-task'); + + await controller.toggleAssistantSkillForSession( + 'unit-skill-source-groups-task', + 'browser-automation', + ); + + final routing = controller.buildExternalAcpRoutingForSessionInternal( + 'unit-skill-source-groups-task', + ); + expect(routing.availableSkills.map((item) => item.id), const [ + 'pdf', + 'browser-automation', + 'gateway-search', + ]); + expect(routing.explicitSkills, const ['browser-automation']); + expect( + controller.assistantSelectedSkillKeysForSession( + 'unit-skill-source-groups-task', + ), + const ['browser-automation'], + ); + + await controller.sendChatMessage( + '打开网页完成检查', + selectedSkillLabels: const [ + 'Browser Automation (browser-automation)', + ], + ); + + expect(fakeGoTaskService.requests, hasLength(1)); + expect(fakeGoTaskService.requests.single.selectedSkills, const [ + 'Browser Automation (browser-automation)', + ]); + }); + test( 'selected bridge skill is passed to task context with stable key', () async { @@ -520,7 +600,7 @@ void main() { }, ); - test('skill selection ignores stale local imported skills', () { + test('skill selection ignores stale non-bridge skill keys', () { final controller = AppController( environmentOverride: const {}, ); @@ -532,16 +612,7 @@ void main() { ); controller.upsertTaskThreadInternal( 'unit-skill-source-task', - importedSkills: const [ - AssistantThreadSkillEntry( - key: '/tmp/local-only/SKILL.md', - label: 'Local Only', - description: 'stale local skill', - sourcePath: '/tmp/local-only/SKILL.md', - sourceLabel: 'local', - ), - ], - selectedSkillKeys: const ['/tmp/local-only/SKILL.md'], + selectedSkillKeys: const ['stale-non-bridge-skill'], selectedSkillsSource: ThreadSelectionSource.explicit, ); controller.skillsControllerInternal.itemsInternal =