diff --git a/frontend/src/app/pages/Workflows/GeneralFacet.tsx b/frontend/src/app/pages/Workflows/GeneralFacet.tsx index 72a4e038..d1fbee8f 100644 --- a/frontend/src/app/pages/Workflows/GeneralFacet.tsx +++ b/frontend/src/app/pages/Workflows/GeneralFacet.tsx @@ -1,9 +1,12 @@ import React from 'react'; import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; import Typography from '@mui/material/Typography'; import InputBase from '@mui/material/InputBase'; import Select from '@mui/material/Select'; import MenuItem from '@mui/material/MenuItem'; +import Tooltip from '@mui/material/Tooltip'; +import DeleteOutlineRounded from '@mui/icons-material/DeleteOutlineRounded'; import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { useAppDispatch } from '@/shared/hooks'; @@ -124,6 +127,33 @@ export default function GeneralFacet({ draft, setDraft }: { draft: Workflow; set }} sx={{ flex: 1, fontSize: INPUT_FS, color: c.text.primary, border: `1px solid ${c.border.subtle}`, borderRadius: `${c.radius.md}px`, px: 1.25, py: 0.6, lineHeight: 1.4 }} /> + 1 ? 'Remove step' : 'Workflow needs at least one step'}> + + { + if (draft.steps.length <= 1) return; + setDraft({ ...draft, steps: draft.steps.filter((_, i) => i !== idx) }); + }} + sx={{ + width: 26, + height: 26, + p: 0, + mt: 0.3, + color: c.text.muted, + flexShrink: 0, + '&:hover': { + color: c.status.error, + bgcolor: c.status.errorBg, + }, + }} + > + + + + ))} diff --git a/frontend/src/app/pages/Workflows/StepList.tsx b/frontend/src/app/pages/Workflows/StepList.tsx index 7d3338e6..9e8caf24 100644 --- a/frontend/src/app/pages/Workflows/StepList.tsx +++ b/frontend/src/app/pages/Workflows/StepList.tsx @@ -11,8 +11,11 @@ import React from 'react'; import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; import TextareaAutosize from '@mui/material/TextareaAutosize'; +import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; +import DeleteOutlineRounded from '@mui/icons-material/DeleteOutlineRounded'; import KeyboardArrowDownRounded from '@mui/icons-material/KeyboardArrowDownRounded'; import CheckRounded from '@mui/icons-material/CheckRounded'; import CloseRounded from '@mui/icons-material/CloseRounded'; @@ -29,6 +32,7 @@ interface Props { framed?: boolean; // Edit mode onChangeStep?: (idx: number, text: string) => void; + onDeleteStep?: (idx: number, id: string) => void; // Expand mode expandable?: boolean; expandedIds?: string[]; @@ -46,7 +50,7 @@ const CONNECTOR_X = CIRCLE_SIZE / 2; export default function StepList(props: Props) { const { - steps, framed, onChangeStep, + steps, framed, onChangeStep, onDeleteStep, expandable, expandedIds, onToggleExpand, stepStatuses, activeStepSubtitle, activeStepDuration, maxVisible = 4, @@ -86,6 +90,42 @@ export default function StepList(props: Props) { const label = (s.label || '').trim() || firstWords(s.text, 6); const rawBody = (s.text || '').trim(); const hasExpandableBody = Boolean(expandable && rawBody); + const canDelete = Boolean(onDeleteStep && steps.length > 1); + const deleteButton = onDeleteStep ? ( + + e.stopPropagation()} + onPointerDown={(e) => e.stopPropagation()} + style={{ display: 'inline-flex' }} + > + { + e.stopPropagation(); + if (canDelete) onDeleteStep(idx, s.id); + }} + onPointerDown={(e) => e.stopPropagation()} + sx={{ + width: 24, + height: 24, + p: 0, + color: c.text.muted, + flexShrink: 0, + opacity: canDelete ? 0.72 : 0.35, + '&:hover': { + color: c.status.error, + bgcolor: c.status.errorBg, + opacity: 1, + }, + }} + > + + + + + ) : null; return ( @@ -151,6 +191,7 @@ export default function StepList(props: Props) { {activeStepDuration} )} + {deleteButton} {hasExpandableBody && !isActive && ( {rawBody} diff --git a/frontend/src/app/pages/Workflows/WorkflowCardSubviews.tsx b/frontend/src/app/pages/Workflows/WorkflowCardSubviews.tsx index 457d3dfb..b4aee099 100644 --- a/frontend/src/app/pages/Workflows/WorkflowCardSubviews.tsx +++ b/frontend/src/app/pages/Workflows/WorkflowCardSubviews.tsx @@ -126,6 +126,21 @@ export function PreviewView({ workflowId, steps, sourceSessionId, initialDraft, dispatch(toggleExpandedStep({ workflowId, stepId })); }, [dispatch, workflowId]); + const onDeleteStep = useCallback((idx: number, stepId: string) => { + if (steps.length <= 1) return; + const nextSteps = steps.filter((_, i) => i !== idx); + dispatch(updateWorkflowCard({ + workflowId, + patch: { + draft: { + ...liveDraft, + steps: nextSteps, + }, + expandedStepIds: (card?.expandedStepIds || []).filter((id) => id !== stepId), + }, + })); + }, [card?.expandedStepIds, dispatch, liveDraft, steps, workflowId]); + const onChangeDescription = useCallback((value: string) => { dispatch(updateWorkflowCard({ workflowId, patch: { draft: { ...liveDraft, description: value } } })); }, [dispatch, workflowId, liveDraft]); @@ -171,7 +186,7 @@ export function PreviewView({ workflowId, steps, sourceSessionId, initialDraft, void onChangeDescription; return ( - + {/* Schedule prompt card. Soft accent tint + calendar icon. Accent is the same color the human-intervention (AskUserQuestion) popup uses. */} @@ -267,6 +282,7 @@ export function SavedView({ workflow, steps, runs, activeRunId }: { workflow: Wo void runs; void activeRunId; const card = useAppSelector((s) => s.workflows.openCards[workflow.id]); const expandedIds = card?.expandedStepIds || []; + const [deletingStepId, setDeletingStepId] = useState(null); const openEditAgent = useCallback(() => { dispatch(updateWorkflowCard({ workflowId: workflow.id, patch: { view: 'edit_agent' } })); }, [dispatch, workflow.id]); @@ -276,6 +292,19 @@ export function SavedView({ workflow, steps, runs, activeRunId }: { workflow: Wo const onToggleStep = useCallback((stepId: string) => { dispatch(toggleExpandedStep({ workflowId: workflow.id, stepId })); }, [dispatch, workflow.id]); + const onDeleteStep = useCallback(async (idx: number, stepId: string) => { + if (workflow.steps.length <= 1 || deletingStepId) return; + setDeletingStepId(stepId); + try { + await dispatch(updateWorkflow({ + id: workflow.id, + patch: { steps: workflow.steps.filter((_, i) => i !== idx) }, + ifMatch: workflow.updated_at || null, + })); + } finally { + setDeletingStepId(null); + } + }, [deletingStepId, dispatch, workflow.id, workflow.steps, workflow.updated_at]); const scheduleLine = workflow.schedule.enabled ? describeSchedule(workflow) : 'Schedule this workflow'; const scheduleClickable = !workflow.schedule.enabled; @@ -288,6 +317,7 @@ export function SavedView({ workflow, steps, runs, activeRunId }: { workflow: Wo expandable expandedIds={expandedIds} onToggleExpand={onToggleStep} + onDeleteStep={onDeleteStep} />