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:
gsxdsm
2026-04-04 05:49:54 -07:00
parent 0f02334ed9
commit e250f99b8b
3 changed files with 141 additions and 1 deletions

View File

@@ -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 ────────────────────────────────────────────

View File

@@ -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;

View File

@@ -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". */