diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json
index 2618d9ef06..c3eb6f1f5d 100644
--- a/plugins/scaffolder/package.json
+++ b/plugins/scaffolder/package.json
@@ -74,6 +74,7 @@
"@backstage/plugin-techdocs-common": "workspace:^",
"@backstage/plugin-techdocs-react": "workspace:^",
"@backstage/types": "workspace:^",
+ "@backstage/ui": "workspace:^",
"@codemirror/language": "^6.0.0",
"@codemirror/legacy-modes": "^6.1.0",
"@codemirror/view": "^6.0.0",
@@ -81,6 +82,7 @@
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
"@react-hookz/web": "^24.0.0",
+ "@remixicon/react": "^4.6.0",
"@rjsf/core": "5.24.13",
"@rjsf/material-ui": "5.24.13",
"@rjsf/utils": "5.24.13",
diff --git a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx
index 16e4bc9158..f40aeb545b 100644
--- a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx
+++ b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx
@@ -58,6 +58,7 @@ import { scaffolderTranslationRef } from '../../translation';
import { entityPresentationApiRef } from '@backstage/plugin-catalog-react';
import { default as reactUseAsync } from 'react-use/esm/useAsync';
import { stringifyEntityRef } from '@backstage/catalog-model';
+import { OngoingTaskContextMenu } from './OngoingTaskContextMenu';
const useStyles = makeStyles(theme => ({
contentWrapper: {
@@ -132,9 +133,119 @@ export function OngoingTaskBody(props: {
output?: ScaffolderTaskOutput;
}>;
}) {
+ const { taskId } = useParams();
+ const taskStream = useTaskEventStream(taskId!);
+ const entityPresentationApi = useApi(entityPresentationApiRef);
+ const { t } = useTranslationRef(scaffolderTranslationRef);
+
+ const [logsVisible, setLogVisibleState] = useState(false);
+ const [buttonBarVisible, setButtonBarVisibleState] = useState(true);
+
+ const { allowed: canCancelTask } = usePermission({
+ permission: taskCancelPermission,
+ resourceRef: taskId,
+ });
+
+ const { allowed: canReadTask } = usePermission({
+ permission: taskReadPermission,
+ resourceRef: taskId,
+ });
+
+ const { allowed: canCreateTask } = usePermission({
+ permission: taskCreatePermission,
+ });
+
+ const isRetryableTask =
+ taskStream.task?.spec.EXPERIMENTAL_recovery?.EXPERIMENTAL_strategy ===
+ 'startOver';
+
+ const canRetry = canReadTask && canCreateTask && isRetryableTask;
+
+ const cancelEnabled = !(taskStream.cancelled || taskStream.completed);
+ const isCancelButtonDisabled = !cancelEnabled || !canCancelTask;
+
+ const { value: presentation } = reactUseAsync(async () => {
+ const templateEntityRef = taskStream.task?.spec.templateInfo?.entityRef;
+ if (!templateEntityRef) {
+ return undefined;
+ }
+ return entityPresentationApi.forEntity(templateEntityRef).promise;
+ }, [entityPresentationApi, taskStream.task?.spec.templateInfo?.entityRef]);
+
+ const templateRouteRef = useRouteRef(selectedTemplateRouteRef);
+ const navigate = useNavigate();
+ const analytics = useAnalytics();
+ const scaffolderApi = useApi(scaffolderApiRef);
+
+ const startOver = useCallback(() => {
+ const { namespace, name } =
+ taskStream.task?.spec.templateInfo?.entity?.metadata ?? {};
+
+ const formData = taskStream.task?.spec.parameters ?? {};
+
+ if (!namespace || !name) {
+ return;
+ }
+
+ analytics.captureEvent('click', `Task has been started over`);
+
+ navigate({
+ pathname: templateRouteRef({
+ namespace,
+ templateName: name,
+ }),
+ search: `?${qs.stringify({ formData: JSON.stringify(formData) })}`,
+ });
+ }, [
+ analytics,
+ navigate,
+ taskStream.task?.spec.parameters,
+ taskStream.task?.spec.templateInfo?.entity?.metadata,
+ templateRouteRef,
+ ]);
+
+ const [, { execute: triggerRetry }] = useAsync(async () => {
+ if (taskId) {
+ analytics.captureEvent('retried', 'Template has been retried');
+ await scaffolderApi.retry?.(taskId);
+ }
+ });
+
+ const [{ status: cancelStatus }, { execute: triggerCancel }] = useAsync(
+ async () => {
+ if (taskId) {
+ analytics.captureEvent('cancelled', 'Template has been cancelled');
+ await scaffolderApi.cancelTask(taskId);
+ }
+ },
+ );
+
return (
-
+
+
);
}
diff --git a/plugins/scaffolder/src/components/OngoingTask/OngoingTaskContextMenu.tsx b/plugins/scaffolder/src/components/OngoingTask/OngoingTaskContextMenu.tsx
new file mode 100644
index 0000000000..9539838590
--- /dev/null
+++ b/plugins/scaffolder/src/components/OngoingTask/OngoingTaskContextMenu.tsx
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2026 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Header, MenuTrigger, ButtonIcon, Menu, MenuItem } from '@backstage/ui';
+import {
+ RiMore2Line,
+ RiFileListLine,
+ RiAddCircleLine,
+ RiRepeatLine,
+ RiReplay10Line,
+ RiCloseCircleLine,
+} from '@remixicon/react';
+import { usePermission } from '@backstage/plugin-permission-react';
+import {
+ taskReadPermission,
+ taskCreatePermission,
+} from '@backstage/plugin-scaffolder-common/alpha';
+import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
+import { scaffolderTranslationRef } from '../../translation';
+
+type OngoingTaskContextMenuProps = {
+ title: string;
+ cancelEnabled?: boolean;
+ canRetry: boolean;
+ isRetryableTask: boolean;
+ logsVisible?: boolean;
+ buttonBarVisible?: boolean;
+ onRetry?: () => void;
+ onStartOver?: () => void;
+ onToggleLogs?: (state: boolean) => void;
+ onToggleButtonBar?: (state: boolean) => void;
+ taskId?: string;
+ isCancelButtonDisabled: boolean;
+ onCancel: () => void;
+};
+
+export function OngoingTaskContextMenu(props: OngoingTaskContextMenuProps) {
+ const {
+ title,
+ cancelEnabled,
+ canRetry,
+ isRetryableTask,
+ logsVisible,
+ buttonBarVisible,
+ onRetry,
+ onStartOver,
+ onToggleLogs,
+ onToggleButtonBar,
+ taskId,
+ } = props;
+ const { t } = useTranslationRef(scaffolderTranslationRef);
+
+ const { allowed: canReadTask } = usePermission({
+ permission: taskReadPermission,
+ resourceRef: taskId,
+ });
+
+ const { allowed: canCreateTask } = usePermission({
+ permission: taskCreatePermission,
+ });
+
+ const canStartOver = canReadTask && canCreateTask;
+
+ return (
+
+ }
+ aria-label="More options"
+ />
+
+
+ }
+ />
+ );
+}
diff --git a/yarn.lock b/yarn.lock
index b9930034bb..835ce66b1f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7031,6 +7031,7 @@ __metadata:
"@backstage/plugin-techdocs-react": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/types": "workspace:^"
+ "@backstage/ui": "workspace:^"
"@codemirror/language": "npm:^6.0.0"
"@codemirror/legacy-modes": "npm:^6.1.0"
"@codemirror/view": "npm:^6.0.0"
@@ -7038,6 +7039,7 @@ __metadata:
"@material-ui/icons": "npm:^4.9.1"
"@material-ui/lab": "npm:4.0.0-alpha.61"
"@react-hookz/web": "npm:^24.0.0"
+ "@remixicon/react": "npm:^4.6.0"
"@rjsf/core": "npm:5.24.13"
"@rjsf/material-ui": "npm:5.24.13"
"@rjsf/utils": "npm:5.24.13"