fix: allow stopping archived tasks
This commit is contained in:
parent
2f8a047798
commit
5909e6518b
@ -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();
|
||||
|
||||
@ -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}',
|
||||
|
||||
@ -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) ...[
|
||||
|
||||
@ -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');
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user