From c57945e2128e71fba8e8e6cb40f4d9f1727be160 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Wed, 20 May 2026 16:38:48 +0800 Subject: [PATCH] fix: require yes before deleting archived tasks --- ...ontroller_desktop_workspace_execution.dart | 12 +++ .../settings_archived_tasks_panel.dart | 95 ++++++++++++++++++- .../settings_archived_tasks_panel_test.dart | 16 ++++ .../assistant_archived_tasks_test.dart | 12 ++- 4 files changed, 132 insertions(+), 3 deletions(-) diff --git a/lib/app/app_controller_desktop_workspace_execution.dart b/lib/app/app_controller_desktop_workspace_execution.dart index 09e15afe..712f5502 100644 --- a/lib/app/app_controller_desktop_workspace_execution.dart +++ b/lib/app/app_controller_desktop_workspace_execution.dart @@ -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, diff --git a/lib/features/settings/settings_archived_tasks_panel.dart b/lib/features/settings/settings_archived_tasks_panel.dart index 51f8f9c5..7c672454 100644 --- a/lib/features/settings/settings_archived_tasks_panel.dart +++ b/lib/features/settings/settings_archived_tasks_panel.dart @@ -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 _confirmDeleteWithYes( + BuildContext context, + GatewaySessionSummary session, + ) async { + final palette = context.palette; + final result = await showDialog( + 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) { diff --git a/test/features/settings/settings_archived_tasks_panel_test.dart b/test/features/settings/settings_archived_tasks_panel_test.dart index c73f1708..f3428fdb 100644 --- a/test/features/settings/settings_archived_tasks_panel_test.dart +++ b/test/features/settings/settings_archived_tasks_panel_test.dart @@ -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')); }); diff --git a/test/runtime/assistant_archived_tasks_test.dart b/test/runtime/assistant_archived_tasks_test.dart index 6bce1891..dfc40fab 100644 --- a/test/runtime/assistant_archived_tasks_test.dart +++ b/test/runtime/assistant_archived_tasks_test.dart @@ -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); }); }); }