xworkmate-app/lib/features/assistant/assistant_page_components.dart
Cowork 3P a908b5118f refactor: remove multi-agent orchestration subsystem (Path B)
Remove the entire multi-agent collaboration execution path, including:
- MultiAgentOrchestrator and its 4-phase pipeline (Architect→Engineer→Tester→Iteration)
- ARIS framework preset and mount infrastructure
- Hardcoded model defaults (kimi-k2.5, minimax-m2.7, glm-5)
- Deprecated runCliPromptInternal() and its fallback call chain
- All related types: MultiAgentConfig, AgentWorkerConfig, MultiAgentRole, etc.

This collapses the architecture to a single clean path:
Flutter → GoTaskServiceClient → ACP Transport → Go Bridge → Remote Execution

2886 lines removed across 41 files.
2026-06-04 05:34:58 +00:00

649 lines
24 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ignore_for_file: unused_import, unnecessary_import
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:path_provider/path_provider.dart';
import 'package:super_clipboard/super_clipboard.dart';
import '../../app/app_controller.dart';
import '../../app/app_metadata.dart';
import '../../app/ui_feature_manifest.dart';
import '../../i18n/app_language.dart';
import '../../models/app_models.dart';
import '../../runtime/runtime_models.dart';
import '../../theme/app_palette.dart';
import '../../theme/app_theme.dart';
import '../../widgets/assistant_focus_panel.dart';
import '../../widgets/assistant_artifact_sidebar.dart';
import '../../widgets/desktop_workspace_scaffold.dart';
import '../../widgets/pane_resize_handle.dart';
import '../../widgets/surface_card.dart';
import 'assistant_page_main.dart';
import 'assistant_page_composer_bar.dart';
import 'assistant_page_composer_state_helpers.dart';
import 'assistant_page_composer_support.dart';
import 'assistant_page_tooltip_labels.dart';
import 'assistant_page_message_widgets.dart';
import 'assistant_page_task_models.dart';
import 'assistant_page_composer_skill_picker.dart';
import 'assistant_page_composer_clipboard.dart';
import 'assistant_page_components_core.dart';
class AssistantTaskRailInternal extends StatefulWidget {
const AssistantTaskRailInternal({
super.key,
required this.controller,
required this.tasks,
required this.query,
required this.searchController,
required this.onQueryChanged,
required this.onClearQuery,
required this.onRefreshTasks,
required this.onCreateTask,
required this.onSelectTask,
required this.onArchiveTask,
required this.onRenameTask,
});
final AppController controller;
final List<AssistantTaskEntryInternal> tasks;
final String query;
final TextEditingController searchController;
final ValueChanged<String> onQueryChanged;
final VoidCallback onClearQuery;
final Future<void> Function() onRefreshTasks;
final Future<void> Function() onCreateTask;
final Future<void> Function(String sessionKey) onSelectTask;
final Future<void> Function(String sessionKey) onArchiveTask;
final Future<void> Function(AssistantTaskEntryInternal entry) onRenameTask;
@override
State<AssistantTaskRailInternal> createState() =>
AssistantTaskRailStateInternal();
}
class AssistantTaskRailStateInternal extends State<AssistantTaskRailInternal> {
final Set<AssistantExecutionTarget> expandedGroupsInternal =
<AssistantExecutionTarget>{};
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final palette = context.palette;
final tasks = widget.tasks;
final groupedTasks = groupTasksForRailInternal(
tasks,
widget.controller.visibleAssistantExecutionTargets(
AssistantExecutionTarget.values,
),
);
final activeCount = tasks.where((task) {
final status = normalizedTaskStatusInternal(task.status);
return status == 'running';
}).length;
final openCount = tasks
.where((task) => normalizedTaskStatusInternal(task.status) == 'open')
.length;
return SurfaceCard(
borderRadius: 0,
padding: EdgeInsets.zero,
tone: SurfaceCardTone.chrome,
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: TextField(
key: const Key('assistant-task-search'),
controller: widget.searchController,
onChanged: widget.onQueryChanged,
decoration: InputDecoration(
hintText: appText('搜索任务', 'Search tasks'),
prefixIcon: const Icon(Icons.search_rounded),
suffixIcon: widget.query.isEmpty
? null
: IconButton(
tooltip: appText('清除搜索', 'Clear search'),
onPressed: widget.onClearQuery,
icon: const Icon(Icons.close_rounded),
),
),
),
),
const SizedBox(width: 6),
IconButton(
key: const Key('assistant-task-refresh'),
tooltip: appText('刷新任务', 'Refresh tasks'),
onPressed: () async {
await widget.onRefreshTasks();
},
icon: const Icon(Icons.refresh_rounded),
),
],
),
const SizedBox(height: 6),
SizedBox(
width: double.infinity,
child: FilledButton.tonalIcon(
key: const Key('assistant-new-task-button'),
onPressed: () async {
await widget.onCreateTask();
},
icon: const Icon(Icons.edit_note_rounded),
label: Text(appText('新对话', 'New conversation')),
style: FilledButton.styleFrom(
minimumSize: const Size(0, 32),
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
const SizedBox(height: 6),
Wrap(
spacing: 6,
runSpacing: 6,
children: [
MetaPillInternal(
label: '${appText('运行中', 'Running')} $activeCount',
icon: Icons.play_circle_outline_rounded,
),
MetaPillInternal(
label: '${appText('当前', 'Open')} $openCount',
icon: Icons.forum_outlined,
),
MetaPillInternal(
label:
'${appText('技能', 'Skills')} ${widget.controller.currentAssistantSkillCount}',
icon: Icons.key_rounded,
),
],
),
],
),
),
Divider(height: 1, color: palette.strokeSoft),
Padding(
padding: const EdgeInsets.fromLTRB(8, 6, 8, 4),
child: Row(
children: [
Text(
appText('任务列表', 'Task list'),
style: theme.textTheme.titleSmall,
),
const SizedBox(width: 6),
Text(
'${tasks.length}',
style: theme.textTheme.bodySmall?.copyWith(
color: palette.textMuted,
),
),
],
),
),
Expanded(
child: ListView.separated(
padding: const EdgeInsets.fromLTRB(4, 0, 4, 4),
itemCount: groupedTasks.length,
separatorBuilder: (_, _) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final group = groupedTasks[index];
final expanded = expandedGroupsInternal.contains(
group.executionTarget,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AssistantTaskGroupHeaderInternal(
executionTarget: group.executionTarget,
count: group.items.length,
expanded: expanded,
onTap: () {
setState(() {
if (expanded) {
expandedGroupsInternal.remove(
group.executionTarget,
);
} else {
expandedGroupsInternal.add(group.executionTarget);
}
});
},
),
if (expanded) ...[
const SizedBox(height: 4),
if (group.items.isEmpty)
Padding(
padding: const EdgeInsets.fromLTRB(28, 0, 8, 4),
child: Text(
appText('当前分组没有任务。', 'No tasks in this group.'),
style: theme.textTheme.bodySmall?.copyWith(
color: palette.textMuted,
),
),
),
for (
var itemIndex = 0;
itemIndex < group.items.length;
itemIndex++
) ...[
if (itemIndex > 0) const SizedBox(height: 4),
AssistantTaskTileInternal(
entry: group.items[itemIndex],
archiveEnabled:
normalizedTaskStatusInternal(
group.items[itemIndex].status,
) !=
'running',
onTap: () async {
await widget.onSelectTask(
group.items[itemIndex].sessionKey,
);
},
onRename: () async {
await widget.onRenameTask(group.items[itemIndex]);
},
onArchive: () async {
await widget.onArchiveTask(
group.items[itemIndex].sessionKey,
);
},
),
],
],
],
);
},
),
),
],
),
);
}
}
List<AssistantTaskGroupInternal> groupTasksForRailInternal(
List<AssistantTaskEntryInternal> tasks,
List<AssistantExecutionTarget> visibleExecutionTargets,
) {
final compactTargets = compactAssistantExecutionTargets(
visibleExecutionTargets,
);
final grouped = <AssistantExecutionTarget, List<AssistantTaskEntryInternal>>{
for (final target in compactTargets) target: <AssistantTaskEntryInternal>[],
};
for (final task in tasks) {
final bucket =
grouped[collapseAssistantExecutionTargetForDisplay(
task.executionTarget,
)];
if (bucket == null) {
continue;
}
bucket.add(task);
}
return compactTargets
.map(
(target) => AssistantTaskGroupInternal(
executionTarget: target,
items: grouped[target]!,
),
)
.toList(growable: false);
}
class AssistantTaskTileInternal extends StatelessWidget {
const AssistantTaskTileInternal({
super.key,
required this.entry,
required this.archiveEnabled,
required this.onTap,
required this.onRename,
required this.onArchive,
});
final AssistantTaskEntryInternal entry;
final bool archiveEnabled;
final VoidCallback onTap;
final VoidCallback onRename;
final VoidCallback onArchive;
@override
Widget build(BuildContext context) {
final palette = context.palette;
final theme = Theme.of(context);
final statusStyle = pillStyleForStatusInternal(context, entry.status);
return Material(
color: entry.isCurrent ? palette.surfacePrimary : Colors.transparent,
borderRadius: BorderRadius.circular(8),
child: InkWell(
key: ValueKey<String>('assistant-task-item-${entry.sessionKey}'),
borderRadius: BorderRadius.circular(8),
onTap: onTap,
onLongPress: onRename,
onSecondaryTap: onRename,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 7),
decoration: BoxDecoration(
color: entry.isCurrent
? palette.surfaceSecondary
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: entry.isCurrent ? palette.strokeSoft : Colors.transparent,
),
),
child: Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: statusStyle.backgroundColor,
borderRadius: BorderRadius.circular(6),
),
child: Icon(
entry.draft
? Icons.edit_note_rounded
: _taskIsActiveInternal(entry.status)
? Icons.play_arrow_rounded
: normalizedTaskStatusInternal(entry.status) ==
'interrupted'
? Icons.pause_circle_outline_rounded
: Icons.task_alt_rounded,
size: 15,
color: statusStyle.foregroundColor,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
entry.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleSmall?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: entry.isCurrent
? FontWeight.w600
: FontWeight.w500,
),
),
),
const SizedBox(width: 8),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
entry.updatedAtLabel,
style: theme.textTheme.bodySmall?.copyWith(
color: palette.textMuted,
),
),
if (_taskShowsProgressInternal(entry.status)) ...[
const SizedBox(height: 4),
SizedBox(
key: ValueKey<String>(
'assistant-task-progress-${entry.sessionKey}',
),
width: 56,
child: LinearProgressIndicator(
value: _taskProgressValueInternal(entry.status),
minHeight: 3,
color: statusStyle.foregroundColor,
backgroundColor: statusStyle.foregroundColor.withValues(
alpha: 0.16,
),
borderRadius: BorderRadius.circular(999),
),
),
],
],
),
const SizedBox(width: 2),
IconButton(
key: ValueKey<String>(
'assistant-task-archive-${entry.sessionKey}',
),
tooltip: appText('归档任务', 'Archive task'),
visualDensity: VisualDensity.compact,
splashRadius: 12,
onPressed: archiveEnabled ? onArchive : null,
icon: Icon(
Icons.archive_outlined,
size: 18,
color: palette.textMuted,
),
),
],
),
),
),
);
}
}
bool _taskIsActiveInternal(String status) {
final normalized = normalizedTaskStatusInternal(status);
return normalized == 'running';
}
bool _taskShowsProgressInternal(String status) {
final normalized = normalizedTaskStatusInternal(status);
return normalized == 'running' || normalized == 'interrupted';
}
double? _taskProgressValueInternal(String status) {
return switch (normalizedTaskStatusInternal(status)) {
'interrupted' => 0.48,
_ => null,
};
}
class AssistantTaskGroupHeaderInternal extends StatelessWidget {
const AssistantTaskGroupHeaderInternal({
super.key,
required this.executionTarget,
required this.count,
required this.expanded,
required this.onTap,
});
final AssistantExecutionTarget executionTarget;
final int count;
final bool expanded;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final palette = context.palette;
final theme = Theme.of(context);
return Material(
color: Colors.transparent,
child: InkWell(
key: ValueKey<String>('assistant-task-group-${executionTarget.name}'),
borderRadius: BorderRadius.circular(8),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.fromLTRB(4, 4, 4, 2),
child: Row(
children: [
Icon(
expanded
? Icons.keyboard_arrow_down_rounded
: Icons.keyboard_arrow_right_rounded,
size: 16,
color: palette.textMuted,
),
const SizedBox(width: 4),
Icon(executionTarget.icon, size: 14, color: palette.textMuted),
const SizedBox(width: 6),
Flexible(
child: Text(
executionTarget.compactLabel,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.labelMedium?.copyWith(
color: palette.textSecondary,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 6),
Text(
'$count',
style: theme.textTheme.bodySmall?.copyWith(
color: palette.textMuted,
),
),
],
),
),
),
);
}
}
class AssistantEmptyStateInternal extends StatelessWidget {
const AssistantEmptyStateInternal({
super.key,
required this.controller,
required this.onFocusComposer,
required this.onOpenGateway,
required this.onOpenAiGatewaySettings,
required this.onReconnectGateway,
});
final AppController controller;
final VoidCallback onFocusComposer;
final VoidCallback onOpenGateway;
final VoidCallback onOpenAiGatewaySettings;
final Future<void> Function() onReconnectGateway;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final connectionState = controller.currentAssistantConnectionState;
final connected = connectionState.connected;
final reconnectAvailable = controller.canQuickConnectGateway;
final title = connected
? appText('开始对话或运行任务', 'Start a chat or run a task')
: connectionState.status == RuntimeConnectionStatus.error
? appText('Bridge 连接失败', 'Bridge connection failed')
: appText('先连接 Bridge', 'Connect xworkmate-bridge first');
final description = connected
? appText(
'输入需求后即可开始执行,结果会回到当前会话并同步到任务页。',
'Type a request to start execution. Results return to this session and the Tasks page.',
)
: connectionState.gatewayTokenMissing
? appText(
'首次连接需要共享 Token配对完成后可继续使用本机的 device token。',
'The first connection requires a shared token; after pairing, this device can continue with its device token.',
)
: connectionState.status == RuntimeConnectionStatus.error
? (connectionState.lastError?.trim().isNotEmpty == true
? connectionState.lastError!.trim()
: appText(
'当前 bridge 连接失败。请重试连接;如果问题持续存在,请检查 bridge 运行状态和本机身份材料。',
'The bridge connection failed. Retry the connection, and if it keeps failing, check bridge health and local device identity material.',
))
: !connected
? appText(
'当前 xworkmate-bridge 尚未连接。请先恢复 bridge 连接,再继续当前任务。',
'xworkmate-bridge is not connected yet. Restore the bridge connection, then continue this task.',
)
: (connectionState.lastError?.trim().isNotEmpty == true
? connectionState.lastError!.trim()
: appText(
'连接后可直接对话、创建任务,并在当前会话查看结果。',
'After connecting, you can chat, create tasks, and read results in this session.',
));
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Container(
key: const Key('assistant-empty-state-card'),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: context.palette.surfacePrimary.withValues(alpha: 0.92),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: context.palette.strokeSoft),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: theme.textTheme.titleMedium),
const SizedBox(height: 6),
Text(description, style: theme.textTheme.bodyMedium),
const SizedBox(height: 8),
Wrap(
spacing: 6,
runSpacing: 6,
children: [
FilledButton.icon(
onPressed: connected
? onFocusComposer
: reconnectAvailable
? () async {
await onReconnectGateway();
}
: onOpenGateway,
icon: Icon(
connected
? Icons.edit_rounded
: reconnectAvailable
? Icons.refresh_rounded
: Icons.link_rounded,
),
label: Text(
connected
? appText('开始输入', 'Start typing')
: reconnectAvailable
? appText('重新连接 Bridge', 'Reconnect bridge')
: appText('连接 Bridge', 'Connect xworkmate-bridge'),
),
style: FilledButton.styleFrom(
minimumSize: const Size(0, 28),
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
],
),
],
),
),
),
),
);
}
}