fix: require yes before deleting archived tasks

This commit is contained in:
Haitao Pan 2026-05-20 16:38:48 +08:00
parent 3b61b68e2d
commit c57945e212
4 changed files with 132 additions and 3 deletions

View File

@ -471,10 +471,22 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
if (record == null || !record.archived) {
return;
}
final workspaceBinding = record.workspaceBinding;
final workspacePath = workspaceBinding.workspacePath.trim();
final wasCurrent = matchesSessionKey(
sessionsControllerInternal.currentSessionKey,
normalizedSessionKey,
);
if (workspaceBinding.workspaceKind == WorkspaceKind.localFs &&
isManagedLocalThreadWorkspacePathInternal(
workspacePath,
normalizedSessionKey,
)) {
final workspaceDirectory = Directory(workspacePath);
if (await workspaceDirectory.exists()) {
await workspaceDirectory.delete(recursive: true);
}
}
taskThreadRepositoryInternal.removeWhere(
(key, _) =>
normalizedAssistantSessionKeyInternal(key) == normalizedSessionKey,

View File

@ -82,8 +82,8 @@ class SettingsArchivedTasksPanel extends StatelessWidget {
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.',
'将从 XWorkmate 中删除「${session.label}」的任务记录、消息状态和本地线程工作目录。此操作不可撤销',
'This removes "${session.label}" from XWorkmate task records, message state, and the local thread workspace. This cannot be undone.',
),
),
actions: [
@ -104,10 +104,101 @@ class SettingsArchivedTasksPanel extends StatelessWidget {
],
),
);
if (result != true || !context.mounted) {
return false;
}
return _confirmDeleteWithYes(context, session);
}
Future<bool> _confirmDeleteWithYes(
BuildContext context,
GatewaySessionSummary session,
) async {
final palette = context.palette;
final result = await showDialog<bool>(
context: context,
builder: (context) =>
_DeleteYesConfirmationDialog(session: session, palette: palette),
);
return result ?? false;
}
}
class _DeleteYesConfirmationDialog extends StatefulWidget {
const _DeleteYesConfirmationDialog({
required this.session,
required this.palette,
});
final GatewaySessionSummary session;
final AppPalette palette;
@override
State<_DeleteYesConfirmationDialog> createState() =>
_DeleteYesConfirmationDialogState();
}
class _DeleteYesConfirmationDialogState
extends State<_DeleteYesConfirmationDialog> {
final TextEditingController _confirmationController = TextEditingController();
bool get _confirmed => _confirmationController.text.trim() == 'Yes';
@override
void dispose() {
_confirmationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(appText('确认彻底删除', 'Confirm permanent delete')),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
appText(
'此操作会删除「${widget.session.label}」的归档记录和任务目录。请输入 Yes 继续。',
'This deletes "${widget.session.label}" archived records and task directory. Type Yes to continue.',
),
),
const SizedBox(height: 14),
TextField(
key: const ValueKey('settings-archived-task-delete-yes-input'),
controller: _confirmationController,
autofocus: true,
onChanged: (_) => setState(() {}),
decoration: InputDecoration(
labelText: appText('输入 Yes', 'Type Yes'),
border: const OutlineInputBorder(),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(appText('取消', 'Cancel')),
),
FilledButton.icon(
key: const ValueKey('settings-archived-task-confirm-delete-yes'),
onPressed: _confirmed ? () => Navigator.of(context).pop(true) : null,
style: FilledButton.styleFrom(
backgroundColor: widget.palette.danger,
foregroundColor: Colors.white,
disabledBackgroundColor: widget.palette.strokeSoft,
disabledForegroundColor: widget.palette.textMuted,
),
icon: const Icon(Icons.delete_forever_outlined),
label: Text(appText('彻底删除', 'Delete permanently')),
),
],
);
}
}
class _ArchivedTasksEmptyState extends StatelessWidget {
@override
Widget build(BuildContext context) {

View File

@ -95,6 +95,22 @@ void main() {
find.byKey(const ValueKey('settings-archived-task-confirm-delete')),
);
await tester.pumpAndSettle();
expect(find.text('确认彻底删除'), findsOneWidget);
expect(
find.byKey(const ValueKey('settings-archived-task-confirm-delete-yes')),
findsOneWidget,
);
expect(calls, isNot(contains('delete:draft:archived-task')));
await tester.enterText(
find.byKey(const ValueKey('settings-archived-task-delete-yes-input')),
'Yes',
);
await tester.pumpAndSettle();
await tester.tap(
find.byKey(const ValueKey('settings-archived-task-confirm-delete-yes')),
);
await tester.pumpAndSettle();
expect(calls, contains('delete:draft:archived-task'));
});

View File

@ -66,7 +66,7 @@ void main() {
},
);
test('deletes only archived task records from controller state', () async {
test('deletes archived task records and local task directory', () async {
final home = await Directory.systemTemp.createTemp(
'xworkmate-delete-archived-task-home-',
);
@ -87,6 +87,15 @@ void main() {
executionTarget: AssistantExecutionTarget.gateway,
messageViewMode: AssistantMessageViewMode.rendered,
);
final workspacePath = controller.assistantWorkspacePathForSession(
sessionKey,
);
expect(workspacePath, isNotEmpty);
final workspaceDirectory = Directory(workspacePath);
await workspaceDirectory.create(recursive: true);
await File(
'${workspaceDirectory.path}/artifact.md',
).writeAsString('archived task artifact');
await controller.saveAssistantTaskArchived(sessionKey, true);
expect(
@ -102,6 +111,7 @@ void main() {
isNot(contains(sessionKey)),
);
expect(controller.hasAssistantTaskStateInternal(sessionKey), isFalse);
expect(await workspaceDirectory.exists(), isFalse);
});
});
}