diff --git a/packages/core/src/workflow/schemas.ts b/packages/core/src/workflow/schemas.ts index 4488197cdb..e5baf88eff 100644 --- a/packages/core/src/workflow/schemas.ts +++ b/packages/core/src/workflow/schemas.ts @@ -61,6 +61,7 @@ export const workflowAction = z prompt: z.string().min(1).max(8_000), adapter: z.enum(["claude", "codex"]).optional(), model: z.string().min(1).optional(), + auto: z.boolean().optional(), }) .strict(); export type WorkflowAction = z.infer; @@ -90,6 +91,7 @@ export const validationDiagnostic = z "duplicate_action_id", "action_empty_prompt", "action_empty_label", + "action_auto_not_bool", ]), message: z.string(), situationId: situationId.optional(), diff --git a/packages/ui/src/features/home/config/ActionEditorPanel.tsx b/packages/ui/src/features/home/config/ActionEditorPanel.tsx index a9b91f1581..cab611fffe 100644 --- a/packages/ui/src/features/home/config/ActionEditorPanel.tsx +++ b/packages/ui/src/features/home/config/ActionEditorPanel.tsx @@ -12,7 +12,14 @@ import { useSettingsStore } from "@posthog/ui/features/settings/settingsStore"; import { usePreviewConfig } from "@posthog/ui/features/task-detail/hooks/usePreviewConfig"; import { Combobox } from "@posthog/ui/primitives/combobox/Combobox"; import type { ComboboxSearchKeys } from "@posthog/ui/primitives/combobox/useComboboxFilter"; -import { Card, Flex, Text, TextArea, TextField } from "@radix-ui/themes"; +import { + Card, + Flex, + Switch, + Text, + TextArea, + TextField, +} from "@radix-ui/themes"; import { useMemo } from "react"; import { SITUATION_TONE } from "./workflowMapLayout"; @@ -189,6 +196,23 @@ export function ActionEditorPanel({ )} + + + + patch({ auto: checked || undefined }) + } + aria-label="Auto-run this action" + /> + + {action.auto + ? "A cloud task starts on its own — skipped while the workstream already has a running task." + : "Run automatically when a workstream enters this state."} + + diff --git a/packages/ui/src/features/home/config/SituationStation.tsx b/packages/ui/src/features/home/config/SituationStation.tsx index 454d8d8ba5..8a4ea8d98f 100644 --- a/packages/ui/src/features/home/config/SituationStation.tsx +++ b/packages/ui/src/features/home/config/SituationStation.tsx @@ -1,4 +1,4 @@ -import { Plus, Sparkle } from "@phosphor-icons/react"; +import { Lightning, Plus, Sparkle } from "@phosphor-icons/react"; import { SITUATIONS, type SituationId, @@ -111,7 +111,15 @@ export function SituationStation({ id, bindings }: Props) { : action.label } > - + {action.auto ? ( + + ) : ( + + )} {action.label || "(no label)"} ); diff --git a/packages/ui/src/features/home/stores/workflowEditorStore.test.ts b/packages/ui/src/features/home/stores/workflowEditorStore.test.ts index dbb736d5d9..61a877be2d 100644 --- a/packages/ui/src/features/home/stores/workflowEditorStore.test.ts +++ b/packages/ui/src/features/home/stores/workflowEditorStore.test.ts @@ -117,6 +117,28 @@ describe("workflowEditorStore", () => { expect(store().draft?.bindings.working[0].label).toBe("Review"); expect(store().dirty).toBe(false); }); + + it("toggles the auto flag and marks dirty", () => { + store().beginEdit(makeConfig({ ci_failing: [makeAction({ id: "a" })] })); + store().updateAction("ci_failing", "a", { auto: true }); + expect(store().draft?.bindings.ci_failing[0].auto).toBe(true); + expect(store().dirty).toBe(true); + store().updateAction("ci_failing", "a", { auto: false }); + expect(store().draft?.bindings.ci_failing[0].auto).toBe(false); + }); + + it("clears dirty when auto is toggled on then back off", () => { + // The editor sends `checked || undefined`, so disabling clears the key + // rather than writing `auto: false`. That restores the exact baseline + // serialization (no `auto` key), so the dirty flag must return to false — + // otherwise an enable→disable round-trip falsely prompts an unsaved-changes save. + store().beginEdit(makeConfig({ ci_failing: [makeAction({ id: "a" })] })); + store().updateAction("ci_failing", "a", { auto: true }); + expect(store().dirty).toBe(true); + store().updateAction("ci_failing", "a", { auto: undefined }); + expect(store().draft?.bindings.ci_failing[0].auto).toBeUndefined(); + expect(store().dirty).toBe(false); + }); }); describe("removeAction", () => {