diff --git a/backend/apps/workflows/executor.py b/backend/apps/workflows/executor.py index 15d84164..66bc194a 100644 --- a/backend/apps/workflows/executor.py +++ b/backend/apps/workflows/executor.py @@ -217,7 +217,7 @@ async def execute( session = None try: - steps = [s for s in wf.steps if s.text and s.text.strip()] + steps = [s for s in wf.steps if s.enabled and s.text and s.text.strip()] if not steps: raise ValueError("Workflow has no steps") diff --git a/backend/apps/workflows/models.py b/backend/apps/workflows/models.py index 7f89a5b7..2c4238f1 100644 --- a/backend/apps/workflows/models.py +++ b/backend/apps/workflows/models.py @@ -83,6 +83,9 @@ class WorkflowStep(BaseModel): # 3 to 6 word LLM-generated headline shown in the collapsed step row. # The full prompt lives in `text`; this is the "at-a-glance" label. label: Optional[str] = None + # Disabled steps stay in the list but the executor skips them, so a user + # can mute a step without losing its prompt. Defaults true for old records. + enabled: bool = True def _empty_str_default() -> str: diff --git a/frontend/src/app/pages/Workflows/app/StepsCard.tsx b/frontend/src/app/pages/Workflows/app/StepsCard.tsx index ba477ec2..65d374ba 100644 --- a/frontend/src/app/pages/Workflows/app/StepsCard.tsx +++ b/frontend/src/app/pages/Workflows/app/StepsCard.tsx @@ -1,26 +1,37 @@ import React, { useEffect, useState } from 'react'; import { useAppDispatch } from '@/shared/hooks'; -import { commitDraft, discardDraft } from '@/shared/state/workflowsSlice'; +import { commitDraft } from '@/shared/state/workflowsSlice'; import type { Workflow, WorkflowStep } from '@/shared/state/workflowsSlice'; import { stepsSignature } from '@/app/pages/Workflows/scheduleUtils'; -import { WC, FONT_SERIF, FONT_SANS } from './uiKit'; +import { useWC, FONT_SERIF, FONT_SANS, track, knob } from './uiKit'; import { useWorkflowPatch } from './useWorkflowPatch'; -interface LocalStep { id: string; label: string; text: string; open: boolean; } +interface LocalStep { id: string; label: string; text: string; open: boolean; enabled: boolean; } function toLocal(steps: WorkflowStep[]): LocalStep[] { - return steps.map((s) => ({ id: s.id, label: s.label || s.text.slice(0, 48), text: s.text, open: false })); + return steps.map((s) => ({ id: s.id, label: s.label || s.text.slice(0, 48), text: s.text, open: false, enabled: s.enabled !== false })); } function newStepId(): string { return `step-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e6).toString(36)}`; } const StepsCard: React.FC<{ workflow: Workflow }> = ({ workflow }) => { + const WC = useWC(); const dispatch = useAppDispatch(); const patch = useWorkflowPatch(); const [local, setLocal] = useState(() => toLocal(workflow.steps)); const [draft, setDraft] = useState(''); + // Agent-proposed step changes apply silently (no Apply/Discard popup): commit + // any staged draft as soon as it lands so the steps just update live. Guarded + // on real content, the edit session snapshots an empty draft on open and + // committing that 400s. + useEffect(() => { + if (workflow.has_draft && (workflow.draft_steps || []).some((s) => s.text && s.text.trim())) { + dispatch(commitDraft({ id: workflow.id, keep_session: true })); + } + }, [workflow.has_draft, workflow.draft_steps, workflow.id, dispatch]); + const sig = stepsSignature(workflow.steps); // Reseed when the server steps change underneath us (commit, agent edit, // another surface) but not on our own in-progress keystrokes. @@ -33,14 +44,19 @@ const StepsCard: React.FC<{ workflow: Workflow }> = ({ workflow }) => { }, [sig]); const commit = (next: LocalStep[]) => { - patch(workflow, { steps: next.map((s) => ({ id: s.id, text: s.text, label: s.label })) }); + patch(workflow, { steps: next.map((s) => ({ id: s.id, text: s.text, label: s.label, enabled: s.enabled })) }); }; const update = (id: string, p: Partial) => setLocal((prev) => prev.map((s) => (s.id === id ? { ...s, ...p } : s))); + const toggleEnabled = (id: string) => { + const next = local.map((s) => (s.id === id ? { ...s, enabled: !s.enabled } : s)); + setLocal(next); + commit(next); + }; const onAdd = () => { const t = draft.trim(); if (!t) return; - const next = [...local, { id: newStepId(), label: t, text: t, open: false }]; + const next = [...local, { id: newStepId(), label: t, text: t, open: false, enabled: true }]; setLocal(next); setDraft(''); commit(next); @@ -52,32 +68,25 @@ const StepsCard: React.FC<{ workflow: Workflow }> = ({ workflow }) => { }; return ( -
+
Steps {local.length} step{local.length === 1 ? '' : 's'}
- {workflow.has_draft && ( -
- The build agent proposed step changes. - - -
- )} -
{local.map((s, i) => ( -
-
+
+
{i + 1} update(s.id, { label: e.target.value })} onBlur={() => commit(local)} placeholder="Step title" - style={{ flex: 1, minWidth: 0, border: 'none', background: 'transparent', padding: 0, fontSize: 13, fontWeight: 600, color: WC.ink }} + style={{ flex: 1, minWidth: 0, border: 'none', background: 'transparent', padding: 0, fontSize: 13, fontWeight: 600, color: WC.ink, textDecoration: s.enabled ? 'none' : 'line-through' }} /> +
toggleEnabled(s.id)} title={s.enabled ? 'Disable step' : 'Enable step'} style={{ ...track(s.enabled, WC), transform: 'scale(0.82)' }}>
update(s.id, { open: !s.open })} style={{ width: 22, height: 22, borderRadius: 6, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', color: WC.muted, flex: 'none' }}>
@@ -85,6 +94,9 @@ const StepsCard: React.FC<{ workflow: Workflow }> = ({ workflow }) => {
+ {!s.open && s.text.trim() && s.text.trim() !== s.label.trim() && ( +
{s.text}
+ )} {s.open && (
Prompt
@@ -93,7 +105,7 @@ const StepsCard: React.FC<{ workflow: Workflow }> = ({ workflow }) => { onChange={(e) => update(s.id, { text: e.target.value })} onBlur={() => commit(local)} placeholder="What should this step do?" - style={{ width: '100%', boxSizing: 'border-box', border: '1px solid rgba(33,30,27,0.12)', borderRadius: 8, background: WC.paper, padding: '9px 11px', fontSize: 12.5, lineHeight: 1.5, color: WC.ink2, resize: 'vertical', minHeight: 76, fontFamily: FONT_SANS }} + style={{ width: '100%', boxSizing: 'border-box', border: `1px solid rgba(${WC.inkRGB},0.12)`, borderRadius: 8, background: WC.paper, padding: '9px 11px', fontSize: 12.5, lineHeight: 1.5, color: WC.ink2, resize: 'vertical', minHeight: 76, fontFamily: FONT_SANS }} />
)} @@ -107,7 +119,7 @@ const StepsCard: React.FC<{ workflow: Workflow }> = ({ workflow }) => { onChange={(e) => setDraft(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); onAdd(); } }} placeholder="Add a step…" - style={{ flex: 1, background: '#FFFFFF', border: '1px solid rgba(33,30,27,0.12)', borderRadius: 8, padding: '8px 11px', fontSize: 13, color: WC.ink }} + style={{ flex: 1, background: WC.raised, border: `1px solid rgba(${WC.inkRGB},0.12)`, borderRadius: 8, padding: '8px 11px', fontSize: 13, color: WC.ink }} />