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) {
|
.map((session) {
|
||||||
final sessionKey = session.key.trim();
|
final sessionKey = session.key.trim();
|
||||||
final preview = session.lastMessagePreview?.trim() ?? '';
|
final preview = session.lastMessagePreview?.trim() ?? '';
|
||||||
|
final thread = controller.taskThreadForSessionInternal(sessionKey);
|
||||||
return SidebarTaskItem(
|
return SidebarTaskItem(
|
||||||
sessionKey: sessionKey,
|
sessionKey: sessionKey,
|
||||||
title: session.label.trim().isEmpty
|
title: session.label.trim().isEmpty
|
||||||
@ -68,6 +69,8 @@ class _AppShellState extends State<AppShell> {
|
|||||||
),
|
),
|
||||||
isCurrent: sessionKey == currentSessionKey,
|
isCurrent: sessionKey == currentSessionKey,
|
||||||
pending: controller.assistantSessionHasPendingRun(sessionKey),
|
pending: controller.assistantSessionHasPendingRun(sessionKey),
|
||||||
|
lifecycleStatus: thread?.lifecycleState.status ?? '',
|
||||||
|
lastResultCode: thread?.lifecycleState.lastResultCode ?? '',
|
||||||
draft: sessionKey.startsWith('draft:'),
|
draft: sessionKey.startsWith('draft:'),
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@ -9,6 +9,8 @@ class SidebarTaskItem {
|
|||||||
required this.executionTarget,
|
required this.executionTarget,
|
||||||
required this.isCurrent,
|
required this.isCurrent,
|
||||||
required this.pending,
|
required this.pending,
|
||||||
|
this.lifecycleStatus = '',
|
||||||
|
this.lastResultCode = '',
|
||||||
this.draft = false,
|
this.draft = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -19,6 +21,8 @@ class SidebarTaskItem {
|
|||||||
final AssistantExecutionTarget executionTarget;
|
final AssistantExecutionTarget executionTarget;
|
||||||
final bool isCurrent;
|
final bool isCurrent;
|
||||||
final bool pending;
|
final bool pending;
|
||||||
|
final String lifecycleStatus;
|
||||||
|
final String lastResultCode;
|
||||||
final bool draft;
|
final bool draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -502,6 +506,7 @@ class _SidebarTaskTile extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final palette = context.palette;
|
final palette = context.palette;
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final statusInfo = _sidebarTaskStatusInfo(item);
|
||||||
return Material(
|
return Material(
|
||||||
color: item.isCurrent ? palette.surfacePrimary : Colors.transparent,
|
color: item.isCurrent ? palette.surfacePrimary : Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
@ -595,6 +600,11 @@ class _SidebarTaskTile extends StatelessWidget {
|
|||||||
color: palette.textMuted,
|
color: palette.textMuted,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (statusInfo != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: _SidebarTaskStatusChip(status: statusInfo),
|
||||||
|
),
|
||||||
if (onArchive != null)
|
if (onArchive != null)
|
||||||
IconButton(
|
IconButton(
|
||||||
key: ValueKey<String>(
|
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) {
|
String _sidebarTaskUpdatedAtLabel(double? updatedAtMs) {
|
||||||
if (updatedAtMs == null) {
|
if (updatedAtMs == null) {
|
||||||
return '';
|
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