feat: manage archived assistant tasks

This commit is contained in:
Haitao Pan 2026-05-20 14:49:22 +08:00
parent 91bf88ed99
commit 9fbbfe881e
11 changed files with 685 additions and 44 deletions

View File

@ -61,6 +61,12 @@ mobile:
build_modes: [debug, profile, release]
description: Mobile bridge and integration settings
ui_surface: settings_page
archived_tasks:
enabled: true
release_tier: stable
build_modes: [debug, profile, release]
description: Mobile archived task management
ui_surface: settings_page
account_access:
enabled: true
release_tier: stable
@ -150,6 +156,12 @@ desktop:
build_modes: [debug, profile, release]
description: Desktop bridge and integration settings
ui_surface: settings_page
archived_tasks:
enabled: true
release_tier: stable
build_modes: [debug, profile, release]
description: Desktop archived task management
ui_surface: settings_page
account_access:
enabled: true
release_tier: stable
@ -239,6 +251,12 @@ web:
build_modes: [debug, profile, release]
description: Web bridge and integration settings
ui_surface: settings_page
archived_tasks:
enabled: true
release_tier: stable
build_modes: [debug, profile, release]
description: Web archived task management
ui_surface: settings_page
account_access:
enabled: true
release_tier: stable

View File

@ -512,6 +512,8 @@ class AppController extends ChangeNotifier {
sessionsControllerInternal.sessions;
List<GatewaySessionSummary> get assistantSessions =>
assistantSessionsInternal();
List<GatewaySessionSummary> get archivedAssistantSessions =>
archivedAssistantSessionsInternal();
List<GatewaySkillSummary> get skills => skillsControllerInternal.items;
List<GatewayModelSummary> get models => modelsControllerInternal.items;
List<GatewayCronJobSummary> get cronJobs => cronJobsControllerInternal.items;

View File

@ -650,6 +650,24 @@ extension AppControllerDesktopThreadSessions on AppController {
);
return items;
}
List<GatewaySessionSummary> archivedAssistantSessionsInternal() {
final items = <GatewaySessionSummary>[];
for (final record in assistantThreadRecordsInternal.values) {
final sessionKey = normalizedAssistantSessionKeyInternal(
record.sessionKey,
);
if (!isAppOwnedAssistantSessionKeyInternal(sessionKey) ||
!record.archived) {
continue;
}
items.add(assistantSessionSummaryForInternal(sessionKey, record: record));
}
items.sort((left, right) {
return (right.updatedAtMs ?? 0).compareTo(left.updatedAtMs ?? 0);
});
return items;
}
}
AssistantExecutionTarget resolveAssistantExecutionTargetFromRecordForTest(

View File

@ -458,4 +458,44 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
recomputeTasksInternal();
notifyIfActiveInternal();
}
Future<void> deleteArchivedAssistantTask(String sessionKey) async {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
);
if (normalizedSessionKey.isEmpty ||
!isAppOwnedAssistantSessionKeyInternal(normalizedSessionKey)) {
return;
}
final record = assistantThreadRecordsInternal[normalizedSessionKey];
if (record == null || !record.archived) {
return;
}
final wasCurrent = matchesSessionKey(
sessionsControllerInternal.currentSessionKey,
normalizedSessionKey,
);
taskThreadRepositoryInternal.removeWhere(
(key, _) =>
normalizedAssistantSessionKeyInternal(key) == normalizedSessionKey,
);
assistantThreadMessagesInternal.remove(normalizedSessionKey);
localSessionMessagesInternal.remove(normalizedSessionKey);
aiGatewayStreamingTextBySessionInternal.remove(normalizedSessionKey);
aiGatewayPendingSessionKeysInternal.remove(normalizedSessionKey);
aiGatewayAbortedSessionKeysInternal.remove(normalizedSessionKey);
assistantThreadTurnQueuesInternal.remove(normalizedSessionKey);
openClawGatewayQueuedTurnsBySessionInternal.remove(normalizedSessionKey);
openClawGatewayQueuedTurnsInternal.removeWhere(
(turn) =>
normalizedAssistantSessionKeyInternal(turn.sessionKey) ==
normalizedSessionKey,
);
recomputeTasksInternal();
if (wasCurrent) {
await ensureActiveAssistantThreadInternal();
}
await flushAssistantThreadPersistenceInternal();
notifyIfActiveInternal();
}
}

View File

@ -46,6 +46,7 @@ abstract final class UiFeatureKeys {
static const assistantLocalRuntime = 'assistant.local_runtime';
static const settingsGateway = 'settings.gateway';
static const settingsArchivedTasks = 'settings.archived_tasks';
static const settingsAccountAccess = 'settings.account_access';
static const settingsVaultServer = 'settings.vault_server';
static const settingsExperimentalCanvas = 'settings.experimental_canvas';
@ -362,7 +363,10 @@ class UiFeatureAccess {
};
static const Map<String, SettingsTab> settingsTabMappingsInternal =
<String, SettingsTab>{UiFeatureKeys.settingsGateway: SettingsTab.gateway};
<String, SettingsTab>{
UiFeatureKeys.settingsGateway: SettingsTab.gateway,
UiFeatureKeys.settingsArchivedTasks: SettingsTab.archivedTasks,
};
bool isEnabledPath(String path) {
final parts = path.split('.');

View File

@ -0,0 +1,251 @@
import 'package:flutter/material.dart';
import '../../i18n/app_language.dart';
import '../../runtime/runtime_models.dart';
import '../../theme/app_palette.dart';
class SettingsArchivedTasksPanel extends StatelessWidget {
const SettingsArchivedTasksPanel({
super.key,
required this.sessions,
required this.onRestore,
required this.onDelete,
});
final List<GatewaySessionSummary> sessions;
final Future<void> Function(String sessionKey) onRestore;
final Future<void> Function(String sessionKey) onDelete;
@override
Widget build(BuildContext context) {
final palette = context.palette;
final theme = Theme.of(context);
return Column(
key: const ValueKey('settings-archived-tasks-panel'),
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Icon(Icons.inventory_2_outlined, color: palette.textSecondary),
const SizedBox(width: 10),
Expanded(
child: Text(
appText('归档任务管理', 'Archived task management'),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
),
Text(
appText('${sessions.length}', '${sessions.length} items'),
style: theme.textTheme.bodySmall?.copyWith(
color: palette.textMuted,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 12),
if (sessions.isEmpty)
_ArchivedTasksEmptyState()
else
Column(
children: [
for (final session in sessions) ...[
_ArchivedTaskTile(
session: session,
onRestore: () => onRestore(session.key),
onDelete: () async {
final confirmed = await _confirmDelete(context, session);
if (confirmed) {
await onDelete(session.key);
}
},
),
if (session != sessions.last)
Divider(height: 1, color: palette.strokeSoft),
],
],
),
],
);
}
Future<bool> _confirmDelete(
BuildContext context,
GatewaySessionSummary session,
) async {
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(appText('彻底删除归档记录', 'Delete archived record')),
content: Text(
appText(
'将从 XWorkmate 中删除「${session.label}」的任务记录和消息状态。此操作不会删除本地线程工作目录里的文件。',
'This removes "${session.label}" from XWorkmate task records and message state. Files in the local thread workspace are not deleted.',
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(appText('取消', 'Cancel')),
),
FilledButton.tonalIcon(
key: const ValueKey('settings-archived-task-confirm-delete'),
onPressed: () => Navigator.of(context).pop(true),
icon: const Icon(Icons.delete_outline_rounded),
label: Text(appText('删除记录', 'Delete record')),
),
],
),
);
return result ?? false;
}
}
class _ArchivedTasksEmptyState extends StatelessWidget {
@override
Widget build(BuildContext context) {
final palette = context.palette;
final theme = Theme.of(context);
return Container(
key: const ValueKey('settings-archived-tasks-empty'),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
decoration: BoxDecoration(
color: palette.surfaceSecondary,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: palette.strokeSoft),
),
child: Column(
children: [
Icon(Icons.archive_outlined, size: 28, color: palette.textMuted),
const SizedBox(height: 8),
Text(
appText('暂无归档任务', 'No archived tasks'),
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
],
),
);
}
}
class _ArchivedTaskTile extends StatelessWidget {
const _ArchivedTaskTile({
required this.session,
required this.onRestore,
required this.onDelete,
});
final GatewaySessionSummary session;
final Future<void> Function() onRestore;
final Future<void> Function() onDelete;
@override
Widget build(BuildContext context) {
final palette = context.palette;
final theme = Theme.of(context);
final preview = session.lastMessagePreview?.trim() ?? '';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: palette.surfaceSecondary,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: palette.strokeSoft),
),
child: Icon(
Icons.archive_outlined,
color: palette.textSecondary,
size: 18,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
session.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
if (preview.isNotEmpty) ...[
const SizedBox(height: 3),
Text(
preview,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: palette.textMuted,
),
),
],
const SizedBox(height: 4),
Text(
_archivedTaskUpdatedAtLabel(session.updatedAtMs),
style: theme.textTheme.bodySmall?.copyWith(
color: palette.textMuted,
),
),
],
),
),
const SizedBox(width: 12),
Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.end,
children: [
FilledButton.tonalIcon(
key: ValueKey<String>(
'settings-archived-task-restore-${session.key}',
),
onPressed: () async {
await onRestore();
},
icon: const Icon(Icons.unarchive_outlined),
label: Text(appText('解除归档', 'Restore')),
),
IconButton(
key: ValueKey<String>(
'settings-archived-task-delete-${session.key}',
),
tooltip: appText('彻底删除归档记录', 'Delete archived record'),
onPressed: () async {
await onDelete();
},
icon: const Icon(Icons.delete_outline_rounded),
),
],
),
],
),
);
}
}
String _archivedTaskUpdatedAtLabel(double? updatedAtMs) {
if (updatedAtMs == null) {
return appText('无更新时间', 'No update time');
}
final timestamp = DateTime.fromMillisecondsSinceEpoch(updatedAtMs.round());
final date =
'${timestamp.year.toString().padLeft(4, '0')}-'
'${timestamp.month.toString().padLeft(2, '0')}-'
'${timestamp.day.toString().padLeft(2, '0')}';
final time =
'${timestamp.hour.toString().padLeft(2, '0')}:'
'${timestamp.minute.toString().padLeft(2, '0')}';
return appText('归档前更新于 $date $time', 'Updated before archive $date $time');
}

View File

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import '../../app/app_controller.dart';
import '../../app/app_metadata.dart';
import '../../app/ui_feature_manifest.dart';
import '../../app/workspace_navigation.dart';
import '../../i18n/app_language.dart';
import '../../models/app_models.dart';
@ -15,6 +16,7 @@ import '../../widgets/settings_page_shell.dart';
import '../../widgets/surface_card.dart';
import 'settings_account_panel.dart';
import 'settings_about_panel.dart';
import 'settings_archived_tasks_panel.dart';
Future<Map<String, dynamic>> loadBridgeMetadataForSettingsAbout({
required Uri bridgeEndpoint,
@ -326,6 +328,14 @@ class _SettingsPageState extends State<SettingsPage> {
});
}
Future<void> _restoreArchivedTask(String sessionKey) async {
await widget.controller.saveAssistantTaskArchived(sessionKey, false);
}
Future<void> _deleteArchivedTask(String sessionKey) async {
await widget.controller.deleteArchivedAssistantTask(sessionKey);
}
Future<SettingsAboutSnapshot> _loadAboutSnapshot() async {
final bridgeMetadata = await _loadBridgeMetadata();
return SettingsAboutSnapshot(
@ -368,13 +378,14 @@ class _SettingsPageState extends State<SettingsPage> {
final accountMfaRequired =
controller.settingsController.accountMfaRequired;
final accountSession = controller.settingsController.accountSession;
final currentTab = controller.settingsTab;
final availableTabs = controller
.featuresFor(resolveUiFeaturePlatformFromContext(context))
.availableSettingsTabs;
return SettingsPageBodyShell(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
breadcrumbs: buildSettingsBreadcrumbs(
controller,
tab: SettingsTab.gateway,
),
breadcrumbs: buildSettingsBreadcrumbs(controller, tab: currentTab),
title: appText('设置', 'Settings'),
subtitle: appText(
'配置 XWorkmate 工作区、网关默认项、界面与诊断选项',
@ -391,44 +402,62 @@ class _SettingsPageState extends State<SettingsPage> {
),
),
bodyChildren: <Widget>[
SurfaceCard(
key: const ValueKey('settings-account-panel-card'),
child: SettingsAccountPanel(
settings: currentSettings,
accountSession: accountSession,
accountState: accountState,
accountBusy: accountBusy,
accountStatus: accountStatus,
accountSignedIn: accountSignedIn,
accountMfaRequired: accountMfaRequired,
accountBaseUrlController: _accountBaseUrlController,
accountIdentifierController: _accountIdentifierController,
accountPasswordController: _accountPasswordController,
accountMfaCodeController: _accountMfaCodeController,
bridgeUrlController: _bridgeUrlController,
bridgeTokenController: _bridgeTokenController,
onSaveAccountProfile: ({required bool isManualBridge}) =>
_persistAccountProfileSettings(
widget.controller.settings,
isManualBridge: isManualBridge,
),
onLogin: () => _loginAccount(widget.controller.settings),
onVerifyMfa: () =>
_verifyAccountMfa(widget.controller.settings),
onCancelMfa: _cancelAccountMfa,
onSync: () => _syncAccount(widget.controller.settings),
onLogout: _logoutAccount,
if (availableTabs.length > 1) ...[
_SettingsTabSelector(
currentTab: currentTab,
availableTabs: availableTabs,
onChanged: (tab) => controller.openSettings(tab: tab),
),
),
const SizedBox(height: 24),
SurfaceCard(
key: const ValueKey('settings-about-panel-card'),
child: SettingsAboutPanel(
snapshot: _aboutSnapshot,
busy: _aboutBusy,
onRefresh: _refreshAboutSnapshot,
const SizedBox(height: 18),
],
if (currentTab == SettingsTab.gateway) ...[
SurfaceCard(
key: const ValueKey('settings-account-panel-card'),
child: SettingsAccountPanel(
settings: currentSettings,
accountSession: accountSession,
accountState: accountState,
accountBusy: accountBusy,
accountStatus: accountStatus,
accountSignedIn: accountSignedIn,
accountMfaRequired: accountMfaRequired,
accountBaseUrlController: _accountBaseUrlController,
accountIdentifierController: _accountIdentifierController,
accountPasswordController: _accountPasswordController,
accountMfaCodeController: _accountMfaCodeController,
bridgeUrlController: _bridgeUrlController,
bridgeTokenController: _bridgeTokenController,
onSaveAccountProfile: ({required bool isManualBridge}) =>
_persistAccountProfileSettings(
widget.controller.settings,
isManualBridge: isManualBridge,
),
onLogin: () => _loginAccount(widget.controller.settings),
onVerifyMfa: () =>
_verifyAccountMfa(widget.controller.settings),
onCancelMfa: _cancelAccountMfa,
onSync: () => _syncAccount(widget.controller.settings),
onLogout: _logoutAccount,
),
),
const SizedBox(height: 24),
SurfaceCard(
key: const ValueKey('settings-about-panel-card'),
child: SettingsAboutPanel(
snapshot: _aboutSnapshot,
busy: _aboutBusy,
onRefresh: _refreshAboutSnapshot,
),
),
] else if (currentTab == SettingsTab.archivedTasks)
SurfaceCard(
key: const ValueKey('settings-archived-tasks-panel-card'),
child: SettingsArchivedTasksPanel(
sessions: controller.archivedAssistantSessions,
onRestore: _restoreArchivedTask,
onDelete: _deleteArchivedTask,
),
),
),
],
);
},
@ -436,6 +465,53 @@ class _SettingsPageState extends State<SettingsPage> {
}
}
class _SettingsTabSelector extends StatelessWidget {
const _SettingsTabSelector({
required this.currentTab,
required this.availableTabs,
required this.onChanged,
});
final SettingsTab currentTab;
final List<SettingsTab> availableTabs;
final ValueChanged<SettingsTab> onChanged;
@override
Widget build(BuildContext context) {
final selectedTab = availableTabs.contains(currentTab)
? currentTab
: availableTabs.first;
return Align(
alignment: Alignment.centerLeft,
child: SegmentedButton<SettingsTab>(
key: const ValueKey('settings-tab-selector'),
segments: [
for (final tab in availableTabs)
ButtonSegment<SettingsTab>(
value: tab,
icon: Icon(
tab == SettingsTab.archivedTasks
? Icons.inventory_2_outlined
: Icons.hub_outlined,
),
label: Text(tab.label),
),
],
selected: <SettingsTab>{selectedTab},
onSelectionChanged: (selection) {
if (selection.isEmpty) {
return;
}
final next = selection.first;
if (next != selectedTab) {
onChanged(next);
}
},
),
);
}
}
String _stringValue(Object? value) {
return value == null ? '' : value.toString().trim();
}

View File

@ -149,11 +149,12 @@ extension AssistantModeCopy on AssistantMode {
};
}
enum SettingsTab { gateway }
enum SettingsTab { gateway, archivedTasks }
extension SettingsTabCopy on SettingsTab {
String get label => switch (this) {
SettingsTab.gateway => appText('集成', 'Integrations'),
SettingsTab.archivedTasks => appText('归档任务', 'Archived tasks'),
};
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:xworkmate/app/app_controller.dart';
import 'package:xworkmate/app/app_shell_desktop.dart';
import 'package:xworkmate/models/app_models.dart';
import 'package:xworkmate/theme/app_theme.dart';
void main() {
@ -14,7 +15,9 @@ void main() {
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final controller = AppController(environmentOverride: const <String, String>{});
final controller = AppController(
environmentOverride: const <String, String>{},
);
addTearDown(controller.dispose);
await tester.pumpWidget(
@ -40,7 +43,9 @@ void main() {
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final controller = AppController(environmentOverride: const <String, String>{});
final controller = AppController(
environmentOverride: const <String, String>{},
);
addTearDown(controller.dispose);
await tester.pumpWidget(
@ -67,6 +72,14 @@ void main() {
find.byKey(const Key('settings-account-panel-card')),
findsOneWidget,
);
controller.openSettings(tab: SettingsTab.archivedTasks);
await tester.pump();
expect(
find.byKey(const Key('settings-archived-tasks-panel-card')),
findsOneWidget,
);
});
});
}

View File

@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:xworkmate/features/settings/settings_archived_tasks_panel.dart';
import 'package:xworkmate/i18n/app_language.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/theme/app_theme.dart';
import 'package:xworkmate/widgets/surface_card.dart';
void main() {
setUp(() {
setActiveAppLanguage(AppLanguage.zh);
});
testWidgets('shows empty state when no archived tasks exist', (tester) async {
await tester.pumpWidget(
_buildTestApp(
child: SettingsArchivedTasksPanel(
sessions: const <GatewaySessionSummary>[],
onRestore: (_) async {},
onDelete: (_) async {},
),
),
);
expect(
find.byKey(const ValueKey('settings-archived-tasks-empty')),
findsOneWidget,
);
expect(find.text('暂无归档任务'), findsOneWidget);
});
testWidgets('restores and confirms deletion for archived task records', (
tester,
) async {
final calls = <String>[];
await tester.pumpWidget(
_buildTestApp(
child: SettingsArchivedTasksPanel(
sessions: const <GatewaySessionSummary>[
GatewaySessionSummary(
key: 'draft:archived-task',
kind: 'assistant',
displayName: '导出 PDF',
surface: 'Assistant',
subject: null,
room: null,
space: null,
updatedAtMs: 1779178980000,
sessionId: 'draft:archived-task',
systemSent: false,
abortedLastRun: false,
thinkingLevel: null,
verboseLevel: null,
inputTokens: null,
outputTokens: null,
totalTokens: null,
model: null,
contextTokens: null,
derivedTitle: '导出 PDF',
lastMessagePreview: '输出为PDF文件',
),
],
onRestore: (sessionKey) async {
calls.add('restore:$sessionKey');
},
onDelete: (sessionKey) async {
calls.add('delete:$sessionKey');
},
),
),
);
expect(find.text('导出 PDF'), findsOneWidget);
expect(find.text('输出为PDF文件'), findsOneWidget);
await tester.tap(
find.byKey(
const ValueKey('settings-archived-task-restore-draft:archived-task'),
),
);
await tester.pump();
expect(calls, contains('restore:draft:archived-task'));
await tester.tap(
find.byKey(
const ValueKey('settings-archived-task-delete-draft:archived-task'),
),
);
await tester.pumpAndSettle();
expect(find.text('彻底删除归档记录'), findsOneWidget);
await tester.tap(
find.byKey(const ValueKey('settings-archived-task-confirm-delete')),
);
await tester.pumpAndSettle();
expect(calls, contains('delete:draft:archived-task'));
});
}
Widget _buildTestApp({required Widget child}) {
return MaterialApp(
theme: AppTheme.light(),
home: Scaffold(
body: Center(
child: SizedBox(width: 720, child: SurfaceCard(child: child)),
),
),
);
}

View File

@ -0,0 +1,107 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:xworkmate/app/app_controller.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
void main() {
group('assistant archived tasks', () {
test(
'lists archived tasks separately and restores them to active sessions',
() async {
final home = await Directory.systemTemp.createTemp(
'xworkmate-archived-task-home-',
);
addTearDown(() async {
if (await home.exists()) {
await home.delete(recursive: true);
}
});
final controller = AppController(
environmentOverride: <String, String>{'HOME': home.path},
);
addTearDown(controller.dispose);
const sessionKey = 'draft:archived-task';
controller.initializeAssistantThreadContext(
sessionKey,
title: '导出 PDF',
executionTarget: AssistantExecutionTarget.gateway,
messageViewMode: AssistantMessageViewMode.rendered,
);
controller.appendAssistantThreadMessageInternal(
sessionKey,
const GatewayChatMessage(
id: 'm-1',
role: 'user',
text: '输出为PDF文件',
timestampMs: 1779178980000,
toolCallId: null,
toolName: null,
stopReason: null,
pending: false,
error: false,
),
);
await controller.saveAssistantTaskArchived(sessionKey, true);
expect(
controller.assistantSessions.map((item) => item.key),
isNot(contains(sessionKey)),
);
expect(
controller.archivedAssistantSessions.map((item) => item.key),
<String>[sessionKey],
);
expect(controller.archivedAssistantSessions.single.label, '导出 PDF');
await controller.saveAssistantTaskArchived(sessionKey, false);
expect(controller.archivedAssistantSessions, isEmpty);
expect(
controller.assistantSessions.map((item) => item.key),
contains(sessionKey),
);
},
);
test('deletes only archived task records from controller state', () async {
final home = await Directory.systemTemp.createTemp(
'xworkmate-delete-archived-task-home-',
);
addTearDown(() async {
if (await home.exists()) {
await home.delete(recursive: true);
}
});
final controller = AppController(
environmentOverride: <String, String>{'HOME': home.path},
);
addTearDown(controller.dispose);
const sessionKey = 'draft:delete-archived-task';
controller.initializeAssistantThreadContext(
sessionKey,
title: '待删除任务',
executionTarget: AssistantExecutionTarget.gateway,
messageViewMode: AssistantMessageViewMode.rendered,
);
await controller.saveAssistantTaskArchived(sessionKey, true);
expect(
controller.archivedAssistantSessions.map((item) => item.key),
<String>[sessionKey],
);
await controller.deleteArchivedAssistantTask(sessionKey);
expect(controller.archivedAssistantSessions, isEmpty);
expect(
controller.assistantSessions.map((item) => item.key),
isNot(contains(sessionKey)),
);
expect(controller.hasAssistantTaskStateInternal(sessionKey), isFalse);
});
});
}