refactor: collapse assistant modes into gateway grouping
This commit is contained in:
parent
b5e0492c5b
commit
4e80afa7d0
@ -286,18 +286,23 @@ List<AssistantTaskGroupInternal> groupTasksForRailInternal(
|
||||
List<AssistantTaskEntryInternal> tasks,
|
||||
List<AssistantExecutionTarget> visibleExecutionTargets,
|
||||
) {
|
||||
final compactTargets = compactAssistantExecutionTargets(
|
||||
visibleExecutionTargets,
|
||||
);
|
||||
final grouped = <AssistantExecutionTarget, List<AssistantTaskEntryInternal>>{
|
||||
for (final target in visibleExecutionTargets)
|
||||
target: <AssistantTaskEntryInternal>[],
|
||||
for (final target in compactTargets) target: <AssistantTaskEntryInternal>[],
|
||||
};
|
||||
for (final task in tasks) {
|
||||
final bucket = grouped[task.executionTarget];
|
||||
final bucket =
|
||||
grouped[collapseAssistantExecutionTargetForDisplay(
|
||||
task.executionTarget,
|
||||
)];
|
||||
if (bucket == null) {
|
||||
continue;
|
||||
}
|
||||
bucket.add(task);
|
||||
}
|
||||
return visibleExecutionTargets
|
||||
return compactTargets
|
||||
.map(
|
||||
(target) => AssistantTaskGroupInternal(
|
||||
executionTarget: target,
|
||||
@ -452,7 +457,7 @@ class AssistantTaskGroupHeaderInternal extends StatelessWidget {
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: Text(
|
||||
executionTarget.label,
|
||||
executionTarget.compactLabel,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
|
||||
@ -365,6 +365,12 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
|
||||
: (visibleExecutionTargets.isNotEmpty
|
||||
? visibleExecutionTargets.first
|
||||
: currentExecutionTarget);
|
||||
final compactExecutionTargets = compactAssistantExecutionTargets(
|
||||
visibleExecutionTargets,
|
||||
);
|
||||
final compactExecutionTarget = collapseAssistantExecutionTargetForDisplay(
|
||||
executionTarget,
|
||||
);
|
||||
final permissionLevel = controller.assistantPermissionLevel;
|
||||
final selectedSkills = widget.availableSkills
|
||||
.where((skill) => widget.selectedSkillKeys.contains(skill.key))
|
||||
@ -418,14 +424,21 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
if (visibleExecutionTargets.isNotEmpty) ...[
|
||||
if (compactExecutionTargets.isNotEmpty) ...[
|
||||
PopupMenuButton<AssistantExecutionTarget>(
|
||||
key: const Key('assistant-execution-target-button'),
|
||||
tooltip: appText('任务对话模式', 'Task Dialog Mode'),
|
||||
onSelected: (value) {
|
||||
controller.setAssistantExecutionTarget(value);
|
||||
final resolvedTarget =
|
||||
value == AssistantExecutionTarget.singleAgent
|
||||
? AssistantExecutionTarget.singleAgent
|
||||
: resolveGatewayExecutionTargetFromVisibleTargets(
|
||||
visibleExecutionTargets,
|
||||
currentTarget: executionTarget,
|
||||
);
|
||||
controller.setAssistantExecutionTarget(resolvedTarget);
|
||||
},
|
||||
itemBuilder: (context) => visibleExecutionTargets
|
||||
itemBuilder: (context) => compactExecutionTargets
|
||||
.map(
|
||||
(value) => PopupMenuItem<AssistantExecutionTarget>(
|
||||
value: value,
|
||||
@ -436,8 +449,8 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
|
||||
children: [
|
||||
Icon(value.icon, size: 18),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Text(value.label)),
|
||||
if (value == executionTarget)
|
||||
Expanded(child: Text(value.compactLabel)),
|
||||
if (value == compactExecutionTarget)
|
||||
const Icon(Icons.check_rounded, size: 18),
|
||||
],
|
||||
),
|
||||
@ -445,8 +458,10 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
|
||||
)
|
||||
.toList(),
|
||||
child: ComposerToolbarChipInternal(
|
||||
icon: executionTarget.icon,
|
||||
tooltip: executionTargetTooltipInternal(executionTarget),
|
||||
icon: compactExecutionTarget.icon,
|
||||
tooltip: executionTargetTooltipInternal(
|
||||
compactExecutionTarget,
|
||||
),
|
||||
showChevron: true,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
@ -796,7 +811,7 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
|
||||
const SizedBox(width: 8),
|
||||
Tooltip(
|
||||
message: submitLabel,
|
||||
child: FilledButton(
|
||||
child: FilledButton(
|
||||
key: const Key('assistant-send-button'),
|
||||
onPressed: connecting
|
||||
? null
|
||||
|
||||
@ -38,7 +38,10 @@ import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
String executionTargetTooltipInternal(AssistantExecutionTarget target) =>
|
||||
appText('任务对话模式: ${target.label}', 'Task dialog mode: ${target.label}');
|
||||
appText(
|
||||
'任务对话模式: ${target.compactLabel}',
|
||||
'Task dialog mode: ${target.compactLabel}',
|
||||
);
|
||||
|
||||
String singleAgentProviderTooltipInternal(SingleAgentProvider provider) =>
|
||||
appText(
|
||||
|
||||
@ -63,6 +63,16 @@ extension AssistantExecutionTargetCopy on AssistantExecutionTarget {
|
||||
AssistantExecutionTarget.remote => 'remote',
|
||||
};
|
||||
|
||||
bool get isGateway =>
|
||||
this == AssistantExecutionTarget.local ||
|
||||
this == AssistantExecutionTarget.remote;
|
||||
|
||||
String get compactLabel => switch (this) {
|
||||
AssistantExecutionTarget.singleAgent => appText('智能体', 'Agent'),
|
||||
AssistantExecutionTarget.local || AssistantExecutionTarget.remote =>
|
||||
appText('OpenClaw Gateway', 'OpenClaw Gateway'),
|
||||
};
|
||||
|
||||
static AssistantExecutionTarget fromJsonValue(String? value) {
|
||||
final normalized = value?.trim() ?? '';
|
||||
switch (normalized) {
|
||||
@ -83,6 +93,49 @@ extension AssistantExecutionTargetCopy on AssistantExecutionTarget {
|
||||
}
|
||||
}
|
||||
|
||||
List<AssistantExecutionTarget> compactAssistantExecutionTargets(
|
||||
Iterable<AssistantExecutionTarget> targets,
|
||||
) {
|
||||
final ordered = <AssistantExecutionTarget>[];
|
||||
var addedGateway = false;
|
||||
for (final target in targets) {
|
||||
if (target == AssistantExecutionTarget.singleAgent) {
|
||||
if (!ordered.contains(AssistantExecutionTarget.singleAgent)) {
|
||||
ordered.add(AssistantExecutionTarget.singleAgent);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!addedGateway) {
|
||||
ordered.add(AssistantExecutionTarget.remote);
|
||||
addedGateway = true;
|
||||
}
|
||||
}
|
||||
return List<AssistantExecutionTarget>.unmodifiable(ordered);
|
||||
}
|
||||
|
||||
AssistantExecutionTarget collapseAssistantExecutionTargetForDisplay(
|
||||
AssistantExecutionTarget target,
|
||||
) => target.isGateway ? AssistantExecutionTarget.remote : target;
|
||||
|
||||
AssistantExecutionTarget resolveGatewayExecutionTargetFromVisibleTargets(
|
||||
Iterable<AssistantExecutionTarget> visibleTargets, {
|
||||
AssistantExecutionTarget? currentTarget,
|
||||
}) {
|
||||
final visible = visibleTargets.toList(growable: false);
|
||||
if (currentTarget != null &&
|
||||
currentTarget.isGateway &&
|
||||
visible.contains(currentTarget)) {
|
||||
return currentTarget;
|
||||
}
|
||||
if (visible.contains(AssistantExecutionTarget.local)) {
|
||||
return AssistantExecutionTarget.local;
|
||||
}
|
||||
if (visible.contains(AssistantExecutionTarget.remote)) {
|
||||
return AssistantExecutionTarget.remote;
|
||||
}
|
||||
return AssistantExecutionTarget.remote;
|
||||
}
|
||||
|
||||
String normalizeSingleAgentProviderId(String value) {
|
||||
final trimmed = value.trim().toLowerCase();
|
||||
if (trimmed.isEmpty) {
|
||||
|
||||
@ -260,26 +260,33 @@ class _SidebarTaskSectionState extends State<SidebarTaskSection> {
|
||||
if (_query.isEmpty) {
|
||||
return widget.items;
|
||||
}
|
||||
return widget.items.where((item) {
|
||||
final haystack = '${item.title}\n${item.preview}\n${item.sessionKey}'
|
||||
.toLowerCase();
|
||||
return haystack.contains(_query);
|
||||
}).toList(growable: false);
|
||||
return widget.items
|
||||
.where((item) {
|
||||
final haystack = '${item.title}\n${item.preview}\n${item.sessionKey}'
|
||||
.toLowerCase();
|
||||
return haystack.contains(_query);
|
||||
})
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
List<_SidebarTaskGroup> _groupedItems(List<SidebarTaskItem> items) {
|
||||
final compactTargets = compactAssistantExecutionTargets(
|
||||
widget.visibleExecutionTargets,
|
||||
);
|
||||
final grouped = <AssistantExecutionTarget, List<SidebarTaskItem>>{
|
||||
for (final target in widget.visibleExecutionTargets)
|
||||
target: <SidebarTaskItem>[],
|
||||
for (final target in compactTargets) target: <SidebarTaskItem>[],
|
||||
};
|
||||
for (final item in items) {
|
||||
final bucket = grouped[item.executionTarget];
|
||||
final bucket =
|
||||
grouped[collapseAssistantExecutionTargetForDisplay(
|
||||
item.executionTarget,
|
||||
)];
|
||||
if (bucket == null) {
|
||||
continue;
|
||||
}
|
||||
bucket.add(item);
|
||||
}
|
||||
return widget.visibleExecutionTargets
|
||||
return compactTargets
|
||||
.map(
|
||||
(target) => _SidebarTaskGroup(
|
||||
executionTarget: target,
|
||||
@ -328,15 +335,14 @@ class _SidebarTaskSectionState extends State<SidebarTaskSection> {
|
||||
if (_expandedTargets.isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
_expandedTargets.addAll(AssistantExecutionTarget.values);
|
||||
_expandedTargets.addAll(
|
||||
compactAssistantExecutionTargets(widget.visibleExecutionTargets),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SidebarTaskGroup {
|
||||
const _SidebarTaskGroup({
|
||||
required this.executionTarget,
|
||||
required this.items,
|
||||
});
|
||||
const _SidebarTaskGroup({required this.executionTarget, required this.items});
|
||||
|
||||
final AssistantExecutionTarget executionTarget;
|
||||
final List<SidebarTaskItem> items;
|
||||
@ -362,7 +368,9 @@ class _SidebarTaskGroupHeader extends StatelessWidget {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
key: ValueKey<String>('workspace-sidebar-task-group-${executionTarget.name}'),
|
||||
key: ValueKey<String>(
|
||||
'workspace-sidebar-task-group-${executionTarget.name}',
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
@ -385,7 +393,7 @@ class _SidebarTaskGroupHeader extends StatelessWidget {
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
executionTarget.label,
|
||||
executionTarget.compactLabel,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
@ -450,7 +458,9 @@ class _SidebarTaskTile extends StatelessWidget {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: item.isCurrent ? palette.surfaceSecondary : Colors.transparent,
|
||||
color: item.isCurrent
|
||||
? palette.surfaceSecondary
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: item.isCurrent ? palette.strokeSoft : Colors.transparent,
|
||||
|
||||
@ -789,7 +789,7 @@ void registerAssistantPageSuiteComposerTestsInternal() {
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('assistant-execution-target-button')),
|
||||
matching: find.text('本地 OpenClaw Gateway'),
|
||||
matching: find.text('OpenClaw Gateway'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
@ -810,15 +810,56 @@ void registerAssistantPageSuiteComposerTestsInternal() {
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('assistant-execution-target-button')),
|
||||
matching: find.text('单机智能体'),
|
||||
matching: find.text('智能体'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.textContaining('单机智能体'), findsWidgets);
|
||||
expect(find.textContaining('智能体'), findsWidgets);
|
||||
},
|
||||
skip: true,
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'AssistantPage collapses execution target menu into agent and gateway modes',
|
||||
(WidgetTester tester) async {
|
||||
final controller = await createControllerWithThreadRecordsInternal(
|
||||
records: <TaskThread>[],
|
||||
useFakeGatewayRuntime: true,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await pumpPage(
|
||||
tester,
|
||||
child: AssistantPage(controller: controller, onOpenDetail: (_) {}),
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(const Key('assistant-new-task-button')));
|
||||
await pumpForUiSyncInternal(tester);
|
||||
|
||||
await tester.tap(
|
||||
find.byKey(const Key('assistant-execution-target-button')),
|
||||
);
|
||||
await pumpForUiSyncInternal(tester);
|
||||
|
||||
expect(
|
||||
find.byKey(
|
||||
const Key('assistant-execution-target-menu-item-singleAgent'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-execution-target-menu-item-remote')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-execution-target-menu-item-local')),
|
||||
findsNothing,
|
||||
);
|
||||
expect(find.text('智能体'), findsWidgets);
|
||||
expect(find.text('OpenClaw Gateway'), findsWidgets);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('AssistantPage shows thread-level message view chip', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
|
||||
@ -270,7 +270,7 @@ void main() {
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'SidebarNavigation only shows configured execution target groups',
|
||||
'SidebarNavigation merges local and remote tasks into one gateway group',
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
@ -350,7 +350,8 @@ void main() {
|
||||
);
|
||||
expect(find.text('单机任务'), findsOneWidget);
|
||||
expect(find.text('远程任务'), findsOneWidget);
|
||||
expect(find.text('本地任务'), findsNothing);
|
||||
expect(find.text('本地任务'), findsOneWidget);
|
||||
expect(find.text('OpenClaw Gateway'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user