feat(FN-838): add defaultOn flag to workflow steps with auto-apply on task creation
- Add defaultOn boolean to WorkflowStep type and store persistence - Auto-apply default-on workflow steps when creating tasks - Refactor AgentDetailView and AgentListModal components for cleaner rendering - Add CSS styles for agent detail and list modal improvements - Update TaskCard to display workflow step status badges - Add comprehensive tests for defaultOn store logic and TaskCard behavior
This commit is contained in:
@@ -4690,6 +4690,117 @@ Task with acceptance criteria
|
||||
expect(found!.mode).toBe("script");
|
||||
expect(found!.scriptName).toBe("typecheck");
|
||||
});
|
||||
|
||||
// ── Workflow Step defaultOn ──────────────────────────────────────────────
|
||||
|
||||
it("should persist defaultOn flag on workflow step creation", async () => {
|
||||
const ws = await store.createWorkflowStep({
|
||||
name: "Default-on Step",
|
||||
description: "Auto-selected for new tasks",
|
||||
defaultOn: true,
|
||||
});
|
||||
|
||||
expect(ws.defaultOn).toBe(true);
|
||||
|
||||
const found = await store.getWorkflowStep(ws.id);
|
||||
expect(found!.defaultOn).toBe(true);
|
||||
|
||||
// Verify persistence
|
||||
const steps = await store.listWorkflowSteps();
|
||||
expect(steps[0].defaultOn).toBe(true);
|
||||
});
|
||||
|
||||
it("should not set defaultOn by default", async () => {
|
||||
const ws = await store.createWorkflowStep({
|
||||
name: "Non-default Step",
|
||||
description: "Not auto-selected",
|
||||
});
|
||||
|
||||
expect(ws.defaultOn).toBeUndefined();
|
||||
|
||||
const found = await store.getWorkflowStep(ws.id);
|
||||
expect(found!.defaultOn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should update defaultOn flag on workflow step", async () => {
|
||||
const ws = await store.createWorkflowStep({
|
||||
name: "Step",
|
||||
description: "Desc",
|
||||
});
|
||||
|
||||
const updated = await store.updateWorkflowStep(ws.id, { defaultOn: true });
|
||||
expect(updated.defaultOn).toBe(true);
|
||||
|
||||
const found = await store.getWorkflowStep(ws.id);
|
||||
expect(found!.defaultOn).toBe(true);
|
||||
});
|
||||
|
||||
it("should clear defaultOn flag by setting to false", async () => {
|
||||
const ws = await store.createWorkflowStep({
|
||||
name: "Step",
|
||||
description: "Desc",
|
||||
defaultOn: true,
|
||||
});
|
||||
|
||||
const updated = await store.updateWorkflowStep(ws.id, { defaultOn: false });
|
||||
expect(updated.defaultOn).toBe(false);
|
||||
|
||||
const found = await store.getWorkflowStep(ws.id);
|
||||
expect(found!.defaultOn).toBe(false);
|
||||
});
|
||||
|
||||
it("should auto-apply default-on workflow steps when creating task without enabledWorkflowSteps", async () => {
|
||||
await store.createWorkflowStep({ name: "Always Run", description: "Auto-select", enabled: true, defaultOn: true });
|
||||
await store.createWorkflowStep({ name: "Optional Check", description: "Only when manually selected", enabled: true, defaultOn: false });
|
||||
await store.createWorkflowStep({ name: "Disabled Step", description: "Disabled step", enabled: false, defaultOn: true });
|
||||
|
||||
const task = await store.createTask({ description: "Test task" });
|
||||
|
||||
// Only the enabled + defaultOn step should be auto-applied
|
||||
expect(task.enabledWorkflowSteps).toEqual(["WS-001"]);
|
||||
});
|
||||
|
||||
it("should use explicit enabledWorkflowSteps over default-on steps", async () => {
|
||||
await store.createWorkflowStep({ name: "Always Run", description: "Auto-select", enabled: true, defaultOn: true });
|
||||
|
||||
const task = await store.createTask({
|
||||
description: "Test task",
|
||||
enabledWorkflowSteps: ["WS-001", "WS-002"],
|
||||
});
|
||||
|
||||
// Explicit input takes precedence
|
||||
expect(task.enabledWorkflowSteps).toEqual(["WS-001", "WS-002"]);
|
||||
});
|
||||
|
||||
it("should use empty enabledWorkflowSteps to override default-on steps", async () => {
|
||||
await store.createWorkflowStep({ name: "Always Run", description: "Auto-select", enabled: true, defaultOn: true });
|
||||
|
||||
const task = await store.createTask({
|
||||
description: "Test task",
|
||||
enabledWorkflowSteps: [],
|
||||
});
|
||||
|
||||
// Explicit empty array means user intentionally wants no steps
|
||||
expect(task.enabledWorkflowSteps).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should not auto-apply disabled steps even with defaultOn flag", async () => {
|
||||
await store.createWorkflowStep({ name: "Disabled Step", description: "Disabled step", enabled: false, defaultOn: true });
|
||||
|
||||
const task = await store.createTask({ description: "Test task" });
|
||||
|
||||
expect(task.enabledWorkflowSteps).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should auto-apply multiple default-on steps in order", async () => {
|
||||
await store.createWorkflowStep({ name: "First", description: "First", enabled: true, defaultOn: true });
|
||||
await store.createWorkflowStep({ name: "Second", description: "Second", enabled: true, defaultOn: true });
|
||||
await store.createWorkflowStep({ name: "Third", description: "Third", enabled: true, defaultOn: false });
|
||||
|
||||
const task = await store.createTask({ description: "Test task" });
|
||||
|
||||
expect(task.enabledWorkflowSteps).toEqual(["WS-001", "WS-002"]);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Title Summarization Tests ────────────────────────────────────────────
|
||||
|
||||
@@ -693,6 +693,27 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
}
|
||||
}
|
||||
|
||||
// Determine enabledWorkflowSteps: explicit input takes precedence, otherwise auto-apply default-on steps
|
||||
let resolvedWorkflowSteps: string[] | undefined = input.enabledWorkflowSteps?.length ? input.enabledWorkflowSteps : undefined;
|
||||
|
||||
// When enabledWorkflowSteps is not provided at all (undefined), auto-apply default-on workflow steps
|
||||
if (input.enabledWorkflowSteps === undefined) {
|
||||
try {
|
||||
const config = await this.readConfig();
|
||||
const defaultOnSteps = (config.workflowSteps || [])
|
||||
.filter((ws) => ws.enabled && ws.defaultOn)
|
||||
.map((ws) => ws.id);
|
||||
if (defaultOnSteps.length > 0) {
|
||||
resolvedWorkflowSteps = defaultOnSteps;
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal: default-on resolution is best-effort
|
||||
}
|
||||
} else if (input.enabledWorkflowSteps.length === 0) {
|
||||
// Explicitly empty array — user intentionally selected no steps
|
||||
resolvedWorkflowSteps = undefined;
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const task: Task = {
|
||||
id,
|
||||
@@ -701,7 +722,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
column: input.column || "triage",
|
||||
dependencies: input.dependencies || [],
|
||||
breakIntoSubtasks: input.breakIntoSubtasks === true ? true : undefined,
|
||||
enabledWorkflowSteps: input.enabledWorkflowSteps?.length ? input.enabledWorkflowSteps : undefined,
|
||||
enabledWorkflowSteps: resolvedWorkflowSteps,
|
||||
modelPresetId: input.modelPresetId,
|
||||
modelProvider: input.modelProvider,
|
||||
modelId: input.modelId,
|
||||
@@ -2547,6 +2568,7 @@ ${stepsSection}`;
|
||||
prompt: mode === "prompt" ? (input.prompt || "") : "",
|
||||
scriptName: mode === "script" ? input.scriptName : undefined,
|
||||
enabled: input.enabled !== undefined ? input.enabled : true,
|
||||
defaultOn: input.defaultOn === true ? true : undefined,
|
||||
modelProvider: mode === "prompt" ? input.modelProvider : undefined,
|
||||
modelId: mode === "prompt" ? input.modelId : undefined,
|
||||
createdAt: now,
|
||||
@@ -2621,6 +2643,7 @@ ${stepsSection}`;
|
||||
if (updates.prompt !== undefined && step.mode === "prompt") step.prompt = updates.prompt;
|
||||
if (updates.scriptName !== undefined && step.mode === "script") step.scriptName = updates.scriptName;
|
||||
if (updates.enabled !== undefined) step.enabled = updates.enabled;
|
||||
if (updates.defaultOn !== undefined) step.defaultOn = updates.defaultOn;
|
||||
if (step.mode === "prompt") {
|
||||
if ("modelProvider" in updates) step.modelProvider = updates.modelProvider;
|
||||
if ("modelId" in updates) step.modelId = updates.modelId;
|
||||
|
||||
@@ -62,6 +62,9 @@ export interface WorkflowStep {
|
||||
scriptName?: string;
|
||||
/** Whether this step is available for selection on new tasks */
|
||||
enabled: boolean;
|
||||
/** When true, this step is automatically pre-selected when creating new tasks.
|
||||
* Users can still deselect it — this only controls the initial default state. */
|
||||
defaultOn?: boolean;
|
||||
/** AI model provider override for the workflow step agent (e.g., "anthropic").
|
||||
* Must be set together with `modelId`. When both model fields are undefined,
|
||||
* the executor uses global settings defaults. Only used when mode is "prompt". */
|
||||
@@ -92,6 +95,9 @@ export interface WorkflowStepInput {
|
||||
scriptName?: string;
|
||||
/** Defaults to true if not specified */
|
||||
enabled?: boolean;
|
||||
/** When true, this step is automatically pre-selected when creating new tasks.
|
||||
* Users can still deselect — this only controls the initial default state. */
|
||||
defaultOn?: boolean;
|
||||
/** AI model provider override. Must be set together with modelId. Only used when mode is "prompt". */
|
||||
modelProvider?: string;
|
||||
/** AI model ID override. Must be set together with modelProvider. Only used when mode is "prompt". */
|
||||
|
||||
Reference in New Issue
Block a user