feat: show sidebar task status chips
This commit is contained in:
parent
5ccf37bec9
commit
0e53058cd3
@ -56,6 +56,7 @@ class _AppShellState extends State<AppShell> {
|
||||
.map((session) {
|
||||
final sessionKey = session.key.trim();
|
||||
final preview = session.lastMessagePreview?.trim() ?? '';
|
||||
final thread = controller.taskThreadForSessionInternal(sessionKey);
|
||||
return SidebarTaskItem(
|
||||
sessionKey: sessionKey,
|
||||
title: session.label.trim().isEmpty
|
||||
@ -68,6 +69,8 @@ class _AppShellState extends State<AppShell> {
|
||||
),
|
||||
isCurrent: sessionKey == currentSessionKey,
|
||||
pending: controller.assistantSessionHasPendingRun(sessionKey),
|
||||
lifecycleStatus: thread?.lifecycleState.status ?? '',
|
||||
lastResultCode: thread?.lifecycleState.lastResultCode ?? '',
|
||||
draft: sessionKey.startsWith('draft:'),
|
||||
);
|
||||
})
|
||||
|
||||
@ -9,6 +9,8 @@ class SidebarTaskItem {
|
||||
required this.executionTarget,
|
||||
required this.isCurrent,
|
||||
required this.pending,
|
||||
this.lifecycleStatus = '',
|
||||
this.lastResultCode = '',
|
||||
this.draft = false,
|
||||
});
|
||||
|
||||
@ -19,6 +21,8 @@ class SidebarTaskItem {
|
||||
final AssistantExecutionTarget executionTarget;
|
||||
final bool isCurrent;
|
||||
final bool pending;
|
||||
final String lifecycleStatus;
|
||||
final String lastResultCode;
|
||||
final bool draft;
|
||||
}
|
||||
|
||||
@ -502,6 +506,7 @@ class _SidebarTaskTile extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final palette = context.palette;
|
||||
final theme = Theme.of(context);
|
||||
final statusInfo = _sidebarTaskStatusInfo(item);
|
||||
return Material(
|
||||
color: item.isCurrent ? palette.surfacePrimary : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@ -595,6 +600,11 @@ class _SidebarTaskTile extends StatelessWidget {
|
||||
color: palette.textMuted,
|
||||
),
|
||||
),
|
||||
if (statusInfo != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: _SidebarTaskStatusChip(status: statusInfo),
|
||||
),
|
||||
if (onArchive != null)
|
||||
IconButton(
|
||||
key: ValueKey<String>(
|
||||
@ -622,6 +632,67 @@ class _SidebarTaskTile extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _SidebarTaskStatusChip extends StatelessWidget {
|
||||
const _SidebarTaskStatusChip({required this.status});
|
||||
|
||||
final StatusInfo status;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = context.palette;
|
||||
final colors = switch (status.tone) {
|
||||
StatusTone.accent => (palette.accentMuted, palette.accent),
|
||||
StatusTone.warning => (palette.surfacePrimary, palette.warning),
|
||||
StatusTone.success => (palette.surfacePrimary, palette.success),
|
||||
StatusTone.danger => (palette.surfacePrimary, palette.danger),
|
||||
StatusTone.neutral => (palette.surfacePrimary, palette.textMuted),
|
||||
};
|
||||
return Container(
|
||||
key: const Key('workspace-sidebar-task-status-chip'),
|
||||
constraints: const BoxConstraints(minHeight: 18),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.$1,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(color: colors.$2.withValues(alpha: 0.22)),
|
||||
),
|
||||
child: Text(
|
||||
status.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: colors.$2,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StatusInfo? _sidebarTaskStatusInfo(SidebarTaskItem item) {
|
||||
if (item.draft && !item.pending) {
|
||||
return null;
|
||||
}
|
||||
final lifecycleStatus = item.lifecycleStatus.trim().toLowerCase();
|
||||
final lastResultCode = item.lastResultCode.trim();
|
||||
final normalizedResultCode = lastResultCode.toLowerCase();
|
||||
if (lifecycleStatus == 'queued' || normalizedResultCode == 'queued') {
|
||||
return StatusInfo(appText('Pending', 'Pending'), StatusTone.warning);
|
||||
}
|
||||
if (item.pending ||
|
||||
lifecycleStatus == 'running' ||
|
||||
normalizedResultCode == 'running') {
|
||||
return StatusInfo(appText('运行', 'Running'), StatusTone.accent);
|
||||
}
|
||||
if (lifecycleStatus == 'ready' &&
|
||||
lastResultCode.isNotEmpty &&
|
||||
normalizedResultCode != 'queued' &&
|
||||
normalizedResultCode != 'running') {
|
||||
return StatusInfo(appText('结束', 'Done'), StatusTone.neutral);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _sidebarTaskUpdatedAtLabel(double? updatedAtMs) {
|
||||
if (updatedAtMs == null) {
|
||||
return '';
|
||||
|
||||
130
test/features/app/sidebar_navigation_task_status_test.dart
Normal file
130
test/features/app/sidebar_navigation_task_status_test.dart
Normal file
@ -0,0 +1,130 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/i18n/app_language.dart';
|
||||
import 'package:xworkmate/models/app_models.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
import 'package:xworkmate/theme/app_theme.dart';
|
||||
import 'package:xworkmate/widgets/sidebar_navigation.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
setActiveAppLanguage(AppLanguage.zh);
|
||||
});
|
||||
|
||||
testWidgets('sidebar task list renders lifecycle status chips', (
|
||||
tester,
|
||||
) async {
|
||||
await _pumpSidebar(
|
||||
tester,
|
||||
items: const <SidebarTaskItem>[
|
||||
SidebarTaskItem(
|
||||
sessionKey: 'running-task',
|
||||
title: '运行任务',
|
||||
preview: '正在执行',
|
||||
updatedAtMs: 1,
|
||||
executionTarget: AssistantExecutionTarget.gateway,
|
||||
isCurrent: false,
|
||||
pending: true,
|
||||
lifecycleStatus: 'running',
|
||||
lastResultCode: 'running',
|
||||
),
|
||||
SidebarTaskItem(
|
||||
sessionKey: 'queued-task',
|
||||
title: '排队任务',
|
||||
preview: '等待执行',
|
||||
updatedAtMs: 1,
|
||||
executionTarget: AssistantExecutionTarget.gateway,
|
||||
isCurrent: false,
|
||||
pending: false,
|
||||
lifecycleStatus: 'queued',
|
||||
lastResultCode: 'queued',
|
||||
),
|
||||
SidebarTaskItem(
|
||||
sessionKey: 'finished-task',
|
||||
title: '结束任务',
|
||||
preview: '已完成',
|
||||
updatedAtMs: 1,
|
||||
executionTarget: AssistantExecutionTarget.gateway,
|
||||
isCurrent: false,
|
||||
pending: false,
|
||||
lifecycleStatus: 'ready',
|
||||
lastResultCode: 'success',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
expect(find.text('运行'), findsOneWidget);
|
||||
expect(find.text('Pending'), findsOneWidget);
|
||||
expect(find.text('结束'), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const Key('workspace-sidebar-task-archive-running-task')),
|
||||
findsNothing,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('workspace-sidebar-task-archive-finished-task')),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('sidebar omits status chip for an idle draft task', (
|
||||
tester,
|
||||
) async {
|
||||
await _pumpSidebar(
|
||||
tester,
|
||||
items: const <SidebarTaskItem>[
|
||||
SidebarTaskItem(
|
||||
sessionKey: 'draft:idle-task',
|
||||
title: '新对话',
|
||||
preview: '',
|
||||
updatedAtMs: 1,
|
||||
executionTarget: AssistantExecutionTarget.gateway,
|
||||
isCurrent: true,
|
||||
pending: false,
|
||||
draft: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
expect(
|
||||
find.byKey(const Key('workspace-sidebar-task-status-chip')),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pumpSidebar(
|
||||
WidgetTester tester, {
|
||||
required List<SidebarTaskItem> items,
|
||||
}) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: Material(
|
||||
child: SizedBox(
|
||||
width: 360,
|
||||
height: 720,
|
||||
child: SidebarNavigation(
|
||||
currentSection: WorkspaceDestination.assistant,
|
||||
sidebarState: AppSidebarState.expanded,
|
||||
appLanguage: AppLanguage.zh,
|
||||
themeMode: ThemeMode.light,
|
||||
onSectionChanged: (_) {},
|
||||
onToggleLanguage: () {},
|
||||
onCycleSidebarState: () {},
|
||||
onExpandFromCollapsed: () {},
|
||||
onOpenAccount: () {},
|
||||
onOpenThemeToggle: () {},
|
||||
accountName: '本地操作员',
|
||||
accountSubtitle: '账号',
|
||||
taskItems: items,
|
||||
visibleExecutionTargets: const <AssistantExecutionTarget>[
|
||||
AssistantExecutionTarget.gateway,
|
||||
],
|
||||
onArchiveTask: (_) async {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user