fix: allow stopping archived tasks

This commit is contained in:
Haitao Pan 2026-06-05 16:59:41 +08:00
parent 2f8a047798
commit 5909e6518b
5 changed files with 101 additions and 14 deletions

View File

@ -1573,20 +1573,7 @@ extension AppControllerDesktopThreadActions on AppController {
return;
}
if (aiGatewayPendingSessionKeysInternal.contains(sessionKey)) {
try {
await goTaskServiceClientInternal.cancelTask(
route: GoTaskServiceRoute.externalAcpSingle,
target: assistantExecutionTargetForSession(sessionKey),
sessionId: sessionKey,
threadId: sessionKey,
association: taskThreadForSessionInternal(
sessionKey,
)?.openClawTaskAssociation,
);
} catch (error) {
debugPrint('OpenClaw cancellation fallback: $error');
// Best effort cancellation only. Local state must still leave pending.
}
await cancelAssistantTaskForSessionInternal(sessionKey);
removeQueuedOpenClawGatewayTurnsForSessionInternal(sessionKey);
removeActiveOpenClawGatewayTurnsForSessionInternal(sessionKey);
markOpenClawGatewayTurnAbortedInternal(sessionKey);
@ -1595,6 +1582,25 @@ extension AppControllerDesktopThreadActions on AppController {
}
}
Future<void> cancelAssistantTaskForSessionInternal(String sessionKey) async {
final normalized = normalizedAssistantSessionKeyInternal(sessionKey);
final association = taskThreadForSessionInternal(
normalized,
)?.openClawTaskAssociation;
try {
await goTaskServiceClientInternal.cancelTask(
route: GoTaskServiceRoute.externalAcpSingle,
target: assistantExecutionTargetForSession(normalized),
sessionId: normalized,
threadId: normalized,
association: association,
);
} catch (error) {
debugPrint('OpenClaw cancellation fallback: $error');
// Best effort cancellation only. Local state must still leave pending.
}
}
Future<void> prepareForExit() async {
try {
await abortRun();

View File

@ -10,11 +10,13 @@ class SettingsArchivedTasksPanel extends StatefulWidget {
required this.sessions,
required this.onRestore,
required this.onDelete,
required this.onStop,
});
final List<GatewaySessionSummary> sessions;
final Future<void> Function(String sessionKey) onRestore;
final Future<void> Function(String sessionKey) onDelete;
final Future<void> Function(String sessionKey)? onStop;
@override
State<SettingsArchivedTasksPanel> createState() =>
@ -152,6 +154,9 @@ class _SettingsArchivedTasksPanelState
onSelectionChanged: (selected) =>
_toggleSessionSelection(session.key, selected),
onRestore: () => widget.onRestore(session.key),
onStop: widget.onStop == null
? null
: () => widget.onStop!(session.key),
onDelete: () async {
final confirmed = await _confirmDelete(context, session);
if (confirmed) {
@ -460,6 +465,7 @@ class _ArchivedTaskTile extends StatelessWidget {
required this.selected,
required this.onSelectionChanged,
required this.onRestore,
required this.onStop,
required this.onDelete,
});
@ -467,6 +473,7 @@ class _ArchivedTaskTile extends StatelessWidget {
final bool selected;
final ValueChanged<bool> onSelectionChanged;
final Future<void> Function() onRestore;
final Future<void> Function()? onStop;
final Future<void> Function() onDelete;
@override
@ -551,6 +558,17 @@ class _ArchivedTaskTile extends StatelessWidget {
icon: const Icon(Icons.unarchive_outlined),
label: Text(appText('解除归档', 'Restore')),
),
if (onStop != null)
FilledButton.tonalIcon(
key: ValueKey<String>(
'settings-archived-task-stop-${session.key}',
),
onPressed: () async {
await onStop!();
},
icon: const Icon(Icons.stop_rounded),
label: Text(appText('停止', 'Stop')),
),
IconButton(
key: ValueKey<String>(
'settings-archived-task-delete-${session.key}',

View File

@ -345,6 +345,10 @@ class _SettingsPageState extends State<SettingsPage> {
await widget.controller.deleteArchivedAssistantTask(sessionKey);
}
Future<void> _stopArchivedTask(String sessionKey) async {
await widget.controller.cancelAssistantTaskForSessionInternal(sessionKey);
}
Future<SettingsAboutSnapshot> _loadAboutSnapshot() async {
final bridgeEndpoint =
widget.controller.resolveGatewayAcpEndpointInternal() ??
@ -523,6 +527,7 @@ class _SettingsPageState extends State<SettingsPage> {
sessions: controller.archivedAssistantSessions,
onRestore: _restoreArchivedTask,
onDelete: _deleteArchivedTask,
onStop: (sessionKey) => _stopArchivedTask(sessionKey),
),
),
] else if (currentTab == SettingsTab.remoteDesktop) ...[

View File

@ -18,6 +18,7 @@ void main() {
sessions: const <GatewaySessionSummary>[],
onRestore: (_) async {},
onDelete: (_) async {},
onStop: null,
),
),
);
@ -66,6 +67,9 @@ void main() {
onDelete: (sessionKey) async {
calls.add('delete:$sessionKey');
},
onStop: (sessionKey) async {
calls.add('stop:$sessionKey');
},
),
),
);
@ -82,6 +86,14 @@ void main() {
expect(calls, contains('restore:draft:archived-task'));
await tester.tap(
find.byKey(
const ValueKey('settings-archived-task-stop-draft:archived-task'),
),
);
await tester.pump();
expect(calls, contains('stop:draft:archived-task'));
await tester.tap(
find.byKey(
const ValueKey('settings-archived-task-delete-draft:archived-task'),
@ -132,6 +144,9 @@ void main() {
onDelete: (sessionKey) async {
calls.add('delete:$sessionKey');
},
onStop: (sessionKey) async {
calls.add('stop:$sessionKey');
},
),
),
);
@ -174,6 +189,9 @@ void main() {
onDelete: (sessionKey) async {
calls.add('delete:$sessionKey');
},
onStop: (sessionKey) async {
calls.add('stop:$sessionKey');
},
),
),
);

View File

@ -3627,6 +3627,46 @@ void main() {
},
);
test(
'cancelAssistantTaskForSessionInternal stops an archived OpenClaw task by session key',
() async {
final fakeGoTaskService = _BlockingGoTaskServiceClient();
final controller = _connectedGatewayController(fakeGoTaskService);
addTearDown(() {
fakeGoTaskService.completeAll();
controller.dispose();
});
const sessionKey = 'archived-running-openclaw';
await _selectGatewaySession(controller, sessionKey);
final pendingFuture = controller.sendChatMessage('stop me');
await _waitForThreadLifecycleStatus(controller, sessionKey, 'running');
await controller.saveAssistantTaskArchived(sessionKey, true);
await controller.cancelAssistantTaskForSessionInternal(sessionKey);
expect(fakeGoTaskService.cancelledSessionIds, <String>[sessionKey]);
expect(
controller.archivedAssistantSessions.map((item) => item.key),
<String>[sessionKey],
);
fakeGoTaskService.complete(
sessionKey,
const GoTaskServiceResult(
success: true,
message: 'stopped',
turnId: 'turn-archived-running-openclaw',
raw: <String, dynamic>{},
errorMessage: '',
resolvedModel: '',
route: GoTaskServiceRoute.externalAcpSingle,
),
);
await pendingFuture;
},
);
test(
'continueAssistantTaskInternal requeues a stopped OpenClaw task without clearing queued work',
() async {