diff --git a/invokeai/app/api/routers/session_queue.py b/invokeai/app/api/routers/session_queue.py
index 6c915b35a43..9331b94d4b3 100644
--- a/invokeai/app/api/routers/session_queue.py
+++ b/invokeai/app/api/routers/session_queue.py
@@ -10,6 +10,7 @@
QUEUE_ITEM_STATUS,
Batch,
BatchStatus,
+ CancelAllExceptCurrentResult,
CancelByBatchIDsResult,
CancelByDestinationResult,
ClearResult,
@@ -94,6 +95,18 @@ async def Pause(
return ApiDependencies.invoker.services.session_processor.pause()
+@session_queue_router.put(
+ "/{queue_id}/cancel_all_except_current",
+ operation_id="cancel_all_except_current",
+ responses={200: {"model": CancelAllExceptCurrentResult}},
+)
+async def cancel_all_except_current(
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+) -> CancelAllExceptCurrentResult:
+ """Immediately cancels all queue items except in-processing items"""
+ return ApiDependencies.invoker.services.session_queue.cancel_all_except_current(queue_id=queue_id)
+
+
@session_queue_router.put(
"/{queue_id}/cancel_by_batch_ids",
operation_id="cancel_by_batch_ids",
diff --git a/invokeai/app/services/session_queue/session_queue_base.py b/invokeai/app/services/session_queue/session_queue_base.py
index 70f6c697fdf..b080d3fdb7b 100644
--- a/invokeai/app/services/session_queue/session_queue_base.py
+++ b/invokeai/app/services/session_queue/session_queue_base.py
@@ -5,6 +5,7 @@
QUEUE_ITEM_STATUS,
Batch,
BatchStatus,
+ CancelAllExceptCurrentResult,
CancelByBatchIDsResult,
CancelByDestinationResult,
CancelByQueueIDResult,
@@ -112,6 +113,11 @@ def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
"""Cancels all queue items with matching queue ID"""
pass
+ @abstractmethod
+ def cancel_all_except_current(self, queue_id: str) -> CancelAllExceptCurrentResult:
+ """Cancels all queue items except in-progress items"""
+ pass
+
@abstractmethod
def list_queue_items(
self,
diff --git a/invokeai/app/services/session_queue/session_queue_common.py b/invokeai/app/services/session_queue/session_queue_common.py
index d43688efad4..8b37c4f8c43 100644
--- a/invokeai/app/services/session_queue/session_queue_common.py
+++ b/invokeai/app/services/session_queue/session_queue_common.py
@@ -366,6 +366,12 @@ class CancelByQueueIDResult(CancelByBatchIDsResult):
pass
+class CancelAllExceptCurrentResult(CancelByBatchIDsResult):
+ """Result of canceling all except current"""
+
+ pass
+
+
class IsEmptyResult(BaseModel):
"""Result of checking if the session queue is empty"""
diff --git a/invokeai/app/services/session_queue/session_queue_sqlite.py b/invokeai/app/services/session_queue/session_queue_sqlite.py
index 5c631b70494..0a646399fef 100644
--- a/invokeai/app/services/session_queue/session_queue_sqlite.py
+++ b/invokeai/app/services/session_queue/session_queue_sqlite.py
@@ -9,6 +9,7 @@
QUEUE_ITEM_STATUS,
Batch,
BatchStatus,
+ CancelAllExceptCurrentResult,
CancelByBatchIDsResult,
CancelByDestinationResult,
CancelByQueueIDResult,
@@ -510,6 +511,39 @@ def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
self.__lock.release()
return CancelByQueueIDResult(canceled=count)
+ def cancel_all_except_current(self, queue_id: str) -> CancelAllExceptCurrentResult:
+ try:
+ where = """--sql
+ WHERE
+ queue_id == ?
+ AND status == 'pending'
+ """
+ self.__lock.acquire()
+ self.__cursor.execute(
+ f"""--sql
+ SELECT COUNT(*)
+ FROM session_queue
+ {where};
+ """,
+ (queue_id,),
+ )
+ count = self.__cursor.fetchone()[0]
+ self.__cursor.execute(
+ f"""--sql
+ UPDATE session_queue
+ SET status = 'canceled'
+ {where};
+ """,
+ (queue_id,),
+ )
+ self.__conn.commit()
+ except Exception:
+ self.__conn.rollback()
+ raise
+ finally:
+ self.__lock.release()
+ return CancelAllExceptCurrentResult(canceled=count)
+
def get_queue_item(self, item_id: int) -> SessionQueueItem:
try:
self.__lock.acquire()
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index 10197fef1ff..96bd9bce291 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -209,9 +209,13 @@
"pauseSucceeded": "Processor Paused",
"pauseFailed": "Problem Pausing Processor",
"cancel": "Cancel",
+ "cancelAllExceptCurrentQueueItemAlertDialog": "Canceling all queue items except the current one will stop pending items but allow the in-progress one to finish.",
+ "cancelAllExceptCurrentQueueItemAlertDialog2": "Are you sure you want to cancel all pending queue items?",
+ "cancelAllExceptCurrentTooltip": "Cancel All Except Current Item",
"cancelTooltip": "Cancel Current Item",
"cancelSucceeded": "Item Canceled",
"cancelFailed": "Problem Canceling Item",
+ "confirm": "Confirm",
"prune": "Prune",
"pruneTooltip": "Prune {{item_count}} Completed Items",
"pruneSucceeded": "Pruned {{item_count}} Completed Items from Queue",
diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx
index 2902c344adb..7bef8d59a6d 100644
--- a/invokeai/frontend/web/src/app/components/App.tsx
+++ b/invokeai/frontend/web/src/app/components/App.tsx
@@ -23,6 +23,7 @@ import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModa
import { ImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/WorkflowListMenu/ShareWorkflowModal';
+import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { DeleteStylePresetDialog } from 'features/stylePresets/components/DeleteStylePresetDialog';
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
@@ -97,6 +98,7 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
+
diff --git a/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog.tsx
new file mode 100644
index 00000000000..cdaaebcdf0e
--- /dev/null
+++ b/invokeai/frontend/web/src/features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog.tsx
@@ -0,0 +1,47 @@
+import { ConfirmationAlertDialog, Text } from '@invoke-ai/ui-library';
+import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
+import { buildUseBoolean } from 'common/hooks/useBoolean';
+import { useCancelAllExceptCurrentQueueItem } from 'features/queue/hooks/useCancelAllExceptCurrentQueueItem';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+const [useCancelAllExceptCurrentQueueItemConfirmationAlertDialog] = buildUseBoolean(false);
+
+export const useCancelAllExceptCurrentQueueItemDialog = () => {
+ const dialog = useCancelAllExceptCurrentQueueItemConfirmationAlertDialog();
+ const { cancelAllExceptCurrentQueueItem, isLoading, isDisabled, queueStatus } = useCancelAllExceptCurrentQueueItem();
+
+ return {
+ cancelAllExceptCurrentQueueItem,
+ isOpen: dialog.isTrue,
+ openDialog: dialog.setTrue,
+ closeDialog: dialog.setFalse,
+ isLoading,
+ queueStatus,
+ isDisabled,
+ };
+};
+
+export const CancelAllExceptCurrentQueueItemConfirmationAlertDialog = memo(() => {
+ useAssertSingleton('CancelAllExceptCurrentQueueItemConfirmationAlertDialog');
+ const { t } = useTranslation();
+ const cancelAllExceptCurrentQueueItem = useCancelAllExceptCurrentQueueItemDialog();
+
+ return (
+
+ {t('queue.cancelAllExceptCurrentQueueItemAlertDialog')}
+
+ {t('queue.cancelAllExceptCurrentQueueItemAlertDialog2')}
+
+ );
+});
+
+CancelAllExceptCurrentQueueItemConfirmationAlertDialog.displayName =
+ 'CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
diff --git a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx
index 27672cec839..2a9f89b0f33 100644
--- a/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx
+++ b/invokeai/frontend/web/src/features/queue/components/QueueActionsMenuButton.tsx
@@ -1,6 +1,7 @@
import { IconButton, Menu, MenuButton, MenuGroup, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { SessionMenuItems } from 'common/components/SessionMenuItems';
+import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { useClearQueueDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { QueueCountBadge } from 'features/queue/components/QueueCountBadge';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
@@ -10,7 +11,15 @@ import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
-import { PiListBold, PiPauseFill, PiPlayFill, PiQueueBold, PiTrashSimpleBold, PiXBold } from 'react-icons/pi';
+import {
+ PiListBold,
+ PiPauseFill,
+ PiPlayFill,
+ PiQueueBold,
+ PiTrashSimpleBold,
+ PiXBold,
+ PiXCircle,
+} from 'react-icons/pi';
export const QueueActionsMenuButton = memo(() => {
const ref = useRef(null);
@@ -18,6 +27,7 @@ export const QueueActionsMenuButton = memo(() => {
const { t } = useTranslation();
const isPauseEnabled = useFeatureStatus('pauseQueue');
const isResumeEnabled = useFeatureStatus('resumeQueue');
+ const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog();
const cancelCurrent = useCancelCurrentQueueItem();
const clearQueue = useClearQueueDialog();
const {
@@ -52,6 +62,15 @@ export const QueueActionsMenuButton = memo(() => {
>
{t('queue.cancelTooltip')}
+ }
+ onClick={cancelAllExceptCurrent.openDialog}
+ isLoading={cancelAllExceptCurrent.isLoading}
+ isDisabled={cancelAllExceptCurrent.isDisabled}
+ >
+ {t('queue.cancelAllExceptCurrentTooltip')}
+
}
diff --git a/invokeai/frontend/web/src/features/queue/hooks/useCancelAllExceptCurrentQueueItem.ts b/invokeai/frontend/web/src/features/queue/hooks/useCancelAllExceptCurrentQueueItem.ts
new file mode 100644
index 00000000000..3d388915457
--- /dev/null
+++ b/invokeai/frontend/web/src/features/queue/hooks/useCancelAllExceptCurrentQueueItem.ts
@@ -0,0 +1,48 @@
+import { useStore } from '@nanostores/react';
+import { toast } from 'features/toast/toast';
+import { useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useCancelAllExceptCurrentMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue';
+import { $isConnected } from 'services/events/stores';
+
+export const useCancelAllExceptCurrentQueueItem = () => {
+ const { t } = useTranslation();
+ const { data: queueStatus } = useGetQueueStatusQuery();
+ const isConnected = useStore($isConnected);
+ const [trigger, { isLoading }] = useCancelAllExceptCurrentMutation({
+ fixedCacheKey: 'cancelAllExceptCurrent',
+ });
+
+ const cancelAllExceptCurrentQueueItem = useCallback(async () => {
+ if (!queueStatus?.queue.pending) {
+ return;
+ }
+
+ try {
+ await trigger().unwrap();
+ toast({
+ id: 'QUEUE_CANCEL_SUCCEEDED',
+ title: t('queue.cancelSucceeded'),
+ status: 'success',
+ });
+ } catch {
+ toast({
+ id: 'QUEUE_CANCEL_FAILED',
+ title: t('queue.cancelFailed'),
+ status: 'error',
+ });
+ }
+ }, [queueStatus?.queue.pending, trigger, t]);
+
+ const isDisabled = useMemo(
+ () => !isConnected || !queueStatus?.queue.pending,
+ [isConnected, queueStatus?.queue.pending]
+ );
+
+ return {
+ cancelAllExceptCurrentQueueItem,
+ isLoading,
+ queueStatus,
+ isDisabled,
+ };
+};
diff --git a/invokeai/frontend/web/src/services/api/endpoints/queue.ts b/invokeai/frontend/web/src/services/api/endpoints/queue.ts
index c2af0d8aa9e..05e5862ee74 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/queue.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/queue.ts
@@ -348,6 +348,25 @@ export const queueApi = api.injectEndpoints({
return ['SessionQueueStatus', 'BatchStatus', { type: 'QueueCountsByDestination', id: destination }];
},
}),
+ cancelAllExceptCurrent: build.mutation<
+ paths['/api/v1/queue/{queue_id}/cancel_all_except_current']['put']['responses']['200']['content']['application/json'],
+ void
+ >({
+ query: () => ({
+ url: buildQueueUrl('cancel_all_except_current'),
+ method: 'PUT',
+ }),
+ onQueryStarted: async (arg, api) => {
+ const { dispatch, queryFulfilled } = api;
+ try {
+ await queryFulfilled;
+ resetListQueryData(dispatch);
+ } catch {
+ // no-op
+ }
+ },
+ invalidatesTags: ['SessionQueueStatus', 'BatchStatus', 'QueueCountsByDestination'],
+ }),
listQueueItems: build.query<
EntityState & {
has_more: boolean;
@@ -390,6 +409,7 @@ export const queueApi = api.injectEndpoints({
});
export const {
+ useCancelAllExceptCurrentMutation,
useCancelByBatchIdsMutation,
useEnqueueBatchMutation,
usePauseProcessorMutation,
diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts
index 7c0250167f8..1f9b90fff6e 100644
--- a/invokeai/frontend/web/src/services/api/schema.ts
+++ b/invokeai/frontend/web/src/services/api/schema.ts
@@ -1102,6 +1102,26 @@ export type paths = {
patch?: never;
trace?: never;
};
+ "/api/v1/queue/{queue_id}/cancel_all_except_current": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /**
+ * Cancel All Except Current
+ * @description Immediately cancels all queue items except in-processing items
+ */
+ put: operations["cancel_all_except_current"];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/api/v1/queue/{queue_id}/cancel_by_batch_ids": {
parameters: {
query?: never;
@@ -3327,6 +3347,17 @@ export type components = {
*/
type: "calculate_image_tiles_output";
};
+ /**
+ * CancelAllExceptCurrentResult
+ * @description Result of canceling all except current
+ */
+ CancelAllExceptCurrentResult: {
+ /**
+ * Canceled
+ * @description Number of queue items canceled
+ */
+ canceled: number;
+ };
/**
* CancelByBatchIDsResult
* @description Result of canceling by list of batch ids
@@ -21174,6 +21205,38 @@ export interface operations {
};
};
};
+ cancel_all_except_current: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description The queue id to perform this operation on */
+ queue_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["CancelAllExceptCurrentResult"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
cancel_by_batch_ids: {
parameters: {
query?: never;