FN-6304: add optional workflow steps

Allow workflows to define default-on optional steps and expose browser verification as a togglable workflow option.

- Add core optional-step helpers, IR metadata, compiler support, exports, and regression coverage.
- Mark browser verification optional in the built-in coding workflow and document optional workflow-step behavior.
- Add dashboard/API support for optional step defaults and enabled-state presentation with localized label cleanup.
- Add a patch changeset for the published Fusion package.

Files changed:
 .changeset/fn-6304-optional-workflow-steps.md      |  5 ++
 docs/task-management.md                            |  2 +-
 docs/workflow-steps.md                             |  8 ++
 packages/core/src/__tests__/workflow-ir.test.ts    | 45 +++++++++++
 .../src/__tests__/workflow-optional-steps.test.ts  | 88 ++++++++++++++++++++++
 packages/core/src/builtin-coding-workflow-ir.ts    |  1 +
 packages/core/src/index.ts                         |  3 +
 packages/core/src/store.ts                         |  3 +
 packages/core/src/workflow-compiler.ts             | 30 +++++++-
 packages/core/src/workflow-ir-types.ts             | 11 +++
 packages/core/src/workflow-ir.ts                   | 32 +++++++-
 packages/core/src/workflow-optional-steps.ts       | 46 +++++++++++
 packages/dashboard/app/api/legacy.ts               | 10 +++
 .../dashboard/app/components/InlineCreateCard.css  | 20 +++++
 .../dashboard/app/components/InlineCreateCard.tsx  | 87 ++++++++++++++++-----
 .../app/components/WorkflowResultsTab.tsx          | 44 +++++++++--
 .../components/__tests__/InlineCreateCard.test.tsx | 41 +++++++++-
 .../__tests__/WorkflowResultsTab.test.tsx          | 58 +++++++++++++-
 .../src/__tests__/workflow-routes.test.ts          | 22 ++++++
 .../src/routes/register-workflow-routes.ts         | 16 +++-
 packages/i18n/locales/en/app.json                  |  6 +-
 packages/i18n/locales/es/app.json                  |  3 -
 packages/i18n/locales/fr/app.json                  |  3 -
 packages/i18n/locales/ko/app.json                  |  3 -
 packages/i18n/locales/zh-CN/app.json               |  3 -
 packages/i18n/locales/zh-TW/app.json               |  3 -
 packages/i18n/src/resources.d.ts                   |  6 +-
 27 files changed, 543 insertions(+), 56 deletions(-)

Fusion-Task-Id: FN-6304

Fusion-Task-Lineage: 385f8ae1-494f-4e2b-a6e9-3a2d2bbe7f71
This commit is contained in:
gsxdsm
2026-06-12 17:57:19 -07:00
parent 4faeed1cda
commit bd87ce7d7e
27 changed files with 543 additions and 56 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": minor
---
Add workflow-declared optional steps and expose Browser Verification as the built-in coding workflow's opt-in optional step for task creation and editing.

View File

@@ -211,7 +211,7 @@ Expand the creation panel (▼) to access additional controls:
- **Agent** — Assign an agent to the task
- **Branch settings** (`branch` / `baseBranch`) remain available in full task forms and task detail editing (not in Quick Entry)
- **Review** — Set review rigor level (None, Plan Only, Plan and Code, Full)
- **Browser Verify** — Enable browser verification workflow step
- **Optional workflow steps** — Enable workflow-declared optional steps for the selected workflow. The built-in coding workflow exposes **Browser Verification** here and keeps it opt-in by default.
### 6) CLI creation

View File

@@ -167,6 +167,14 @@ Notification delivery is intentionally best-effort: a missing/unconfigured notif
Workflows declare typed task fields via IR `fields: [{ id, name, type, required?, default?, options?, render? }]` (`type ∈ string | text | number | boolean | enum | multi-enum | date | url`; `options` for enum kinds; `render.placement ∈ card | detail | detail-section`, `render.widget`, `render.badge`). Values live in `tasks.customFields` and are validated through a single store authority (`updateTaskCustomFields`) with typed rejections (offending `fieldId` + `code`). Editing or switching a workflow **orphans** (never destroys) values for removed/incompatible fields — orphans are retained and shown under a detail disclosure. The task UI renders the schema dynamically (detail-form widgets by type, up to 3 card badges by placement). Agents read/write fields via `fn_task_update`'s `custom_fields` patch; authors set them via `fn_workflow_create/update`. Field values are surfaced in task/session context.
#### Workflow-declared optional steps
Workflows can advertise optional workflow-step templates with `optionalSteps: [{ templateId, defaultOn? }]`. This IR facet is execution-inert: the graph executor does not run or route on `optionalSteps` directly. Instead, the create and task-detail workflow UIs resolve each `templateId` against built-in and plugin-contributed `WorkflowStepTemplate` metadata, show toggleable rows, and persist the selected template ids through the existing per-task `enabledWorkflowSteps` contract.
`defaultOn` is optional and seeds the UI toggle when a task is created or edited before the task has made its own selection. Unknown or removed template ids are skipped during resolution so stale declarations do not render blank controls or break workflow loading.
The built-in coding workflow (`builtin:coding`) declares `browser-verification` as an optional step. It remains opt-in by default, so browser verification only runs for tasks whose `enabledWorkflowSteps` includes `browser-verification`.
## What They Are
A workflow step is a reusable check (AI prompt or script) that can be enabled on tasks.

View File

@@ -75,6 +75,51 @@ describe("parseWorkflowIr — v2 columns & placement", () => {
});
});
describe("parseWorkflowIr — optionalSteps", () => {
const columns = DEFAULT_WORKFLOW_COLUMN_IDS.map((id) => ({ id, name: id, traits: [] }));
const base = (): WorkflowIrV2 => v2(
columns,
[
{ id: "start", kind: "start", column: "todo" },
{ id: "end", kind: "end", column: "todo" },
],
[{ from: "start", to: "end" }],
);
it("parses and serializes optionalSteps deterministically", () => {
const ir: WorkflowIrV2 = {
...base(),
optionalSteps: [
{ templateId: "browser-verification" },
{ templateId: "plugin:example:step", defaultOn: true },
],
};
const parsed = parseWorkflowIr(ir);
expect(parsed).toEqual(ir);
expect(JSON.parse(serializeWorkflowIr(parsed))).toEqual(ir);
});
it("rejects malformed optionalSteps", () => {
expect(() => parseWorkflowIr({ ...base(), optionalSteps: "nope" } as unknown as WorkflowIr)).toThrow(WorkflowIrError);
expect(() => parseWorkflowIr({ ...base(), optionalSteps: [{}] } as unknown as WorkflowIr)).toThrow(/non-empty templateId/);
expect(() => parseWorkflowIr({ ...base(), optionalSteps: [{ templateId: "" }] } as unknown as WorkflowIr)).toThrow(/non-empty templateId/);
expect(() => parseWorkflowIr({ ...base(), optionalSteps: [{ templateId: "browser-verification", defaultOn: "yes" }] } as unknown as WorkflowIr)).toThrow(/defaultOn must be a boolean/);
});
it("upgrades v1 graphs without optionalSteps", () => {
const parsed = parseWorkflowIr({
version: "v1",
name: "legacy",
nodes: startEnd,
edges: [{ from: "start", to: "end" }],
});
expect(parsed.version).toBe("v2");
if (parsed.version !== "v2") throw new Error("expected v2");
expect(parsed.optionalSteps).toBeUndefined();
});
});
describe("parseWorkflowIr — v1 upgrade", () => {
const v1: WorkflowIrV1 = {
version: "v1",

View File

@@ -0,0 +1,88 @@
import { describe, expect, it } from "vitest";
import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js";
import { resolveWorkflowOptionalSteps } from "../workflow-optional-steps.js";
import type { WorkflowIr, WorkflowIrV2 } from "../workflow-ir-types.js";
const v1: WorkflowIr = {
version: "v1",
name: "legacy",
nodes: [
{ id: "start", kind: "start" },
{ id: "end", kind: "end" },
],
edges: [{ from: "start", to: "end" }],
};
function v2(optionalSteps?: WorkflowIrV2["optionalSteps"]): WorkflowIrV2 {
return {
version: "v2",
name: "optional",
columns: [{ id: "todo", name: "Todo", traits: [] }],
nodes: [
{ id: "start", kind: "start", column: "todo" },
{ id: "end", kind: "end", column: "todo" },
],
edges: [{ from: "start", to: "end" }],
optionalSteps,
};
}
describe("resolveWorkflowOptionalSteps", () => {
it("resolves the builtin coding browser verification optional step", () => {
expect(resolveWorkflowOptionalSteps(BUILTIN_CODING_WORKFLOW_IR)).toEqual([
{
templateId: "browser-verification",
name: "Browser Verification",
description: "Verify web application functionality using browser automation",
icon: "globe",
phase: "pre-merge",
defaultOn: false,
},
]);
});
it("skips unknown template ids", () => {
expect(
resolveWorkflowOptionalSteps(v2([
{ templateId: "missing" },
{ templateId: "browser-verification" },
])),
).toHaveLength(1);
});
it("returns an empty array for v1 and v2 workflows without optional steps", () => {
expect(resolveWorkflowOptionalSteps(v1)).toEqual([]);
expect(resolveWorkflowOptionalSteps(v2())).toEqual([]);
});
it("preserves declaration order and resolves plugin templates", () => {
const result = resolveWorkflowOptionalSteps(
v2([
{ templateId: "plugin:demo:first", defaultOn: true },
{ templateId: "browser-verification" },
]),
[
{
id: "plugin:demo:first",
name: "Plugin First",
description: "Plugin optional verification",
prompt: "Run plugin verification",
category: "Quality",
icon: "plug",
phase: "post-merge",
},
],
);
expect(result.map((step) => step.templateId)).toEqual([
"plugin:demo:first",
"browser-verification",
]);
expect(result[0]).toMatchObject({
name: "Plugin First",
icon: "plug",
phase: "post-merge",
defaultOn: true,
});
});
});

View File

@@ -120,6 +120,7 @@ const RAW_BUILTIN_CODING_WORKFLOW_IR: WorkflowIr = {
// Workflow-settings (U1, R4): declare the full moved-key catalog with defaults
// byte-equal to today's DEFAULT_PROJECT_SETTINGS literals. Inert until U3.
settings: BUILTIN_WORKFLOW_SETTINGS,
optionalSteps: [{ templateId: "browser-verification" }],
};
export const BUILTIN_CODING_WORKFLOW_IR = parseWorkflowIr(RAW_BUILTIN_CODING_WORKFLOW_IR);

View File

@@ -79,6 +79,7 @@ export type {
WorkflowFieldOption,
WorkflowFieldRender,
// Workflow-settings (U1): typed setting declaration IR types.
WorkflowOptionalStep,
WorkflowSettingDefinition,
WorkflowSettingType,
WorkflowSettingOption,
@@ -104,6 +105,8 @@ export type {
EffectiveAgentResult,
} from "./column-agent-resolver.js";
export { BUILTIN_CODING_WORKFLOW_IR } from "./builtin-coding-workflow-ir.js";
export { resolveWorkflowOptionalSteps } from "./workflow-optional-steps.js";
export type { ResolvedWorkflowOptionalStep } from "./workflow-optional-steps.js";
export { BUILTIN_STEPWISE_CODING_WORKFLOW_IR } from "./builtin-stepwise-coding-workflow-ir.js";
export { BUILTIN_PR_WORKFLOW_IR } from "./builtin-pr-workflow-ir.js";
export {

View File

@@ -14890,6 +14890,9 @@ ${stepsSection}`;
if (isBuiltinWorkflowId(workflowId) && isInterpreterDeferredWorkflowCompileError(err)) return undefined;
throw err;
}
if (isBuiltinWorkflowId(workflowId) && inputs.length === 0) {
return undefined;
}
const stepIds = await this.materializeWorkflowSteps(workflowId, inputs);
return { workflowId, stepIds };
}

View File

@@ -44,6 +44,23 @@ function isMergeRegionKind(node: WorkflowIrNode): boolean {
* not emitted as steps. */
const SEAM_NAMES = new Set(["planning", "execute", "workflow-step", "review", "merge"]);
const ENGINE_PRIMITIVE_NODE_KINDS = new Set<WorkflowIrNode["kind"]>([
"merge-gate",
"merge-attempt",
"manual-merge-hold",
"retry-backoff",
"recovery-router",
"branch-group-member-integration",
"branch-group-promotion",
"pr-create",
"pr-respond",
"pr-merge",
]);
function isEnginePrimitive(node: WorkflowIrNode): boolean {
return ENGINE_PRIMITIVE_NODE_KINDS.has(node.kind);
}
function seamOf(node: WorkflowIrNode): string | undefined {
const seam = node.config?.seam;
return typeof seam === "string" && SEAM_NAMES.has(seam) ? seam : undefined;
@@ -100,6 +117,9 @@ export function validateLinearity(ir: WorkflowIr): WorkflowCompileError | null {
if (outs.length > 0) return new WorkflowCompileError("end node must have no outgoing edges");
continue;
}
if (isEnginePrimitive(node)) {
continue;
}
if (isMergeRegionKind(node)) {
continue;
@@ -179,7 +199,7 @@ export function validateLinearity(ir: WorkflowIr): WorkflowCompileError | null {
seenSeams.add(seam);
nextExpectedSeamIndex += 1;
}
if (cursor === endNode.id) {
if (cursor === endNode.id || (node && isEnginePrimitive(node))) {
reachedTerminal = true;
break;
}
@@ -188,9 +208,7 @@ export function validateLinearity(ir: WorkflowIr): WorkflowCompileError | null {
if (!reachedTerminal) {
return new WorkflowCompileError("workflow main path does not reach the end node");
}
const unreached = ir.nodes.filter(
(node) => !visited.has(node.id) && node.kind !== "end" && !isMergeRegionKind(node),
);
const unreached = ir.nodes.filter((node) => !visited.has(node.id) && node.kind !== "end" && !isEnginePrimitive(node));
if (unreached.length > 0) {
return new WorkflowCompileError(
`node '${unreached[0].id}' is not on the main path — disconnected nodes ${WORKFLOW_INTERPRETER_DEFERRED_SUFFIX}`,
@@ -283,6 +301,10 @@ export function compileWorkflowToSteps(ir: WorkflowIr): WorkflowStepInput[] {
}
const seam = seamOf(node);
if (isEnginePrimitive(node)) {
break;
}
if (seam === "merge") {
phase = "post-merge";
} else if (!seam && node.kind !== "start" && node.kind !== "end") {

View File

@@ -311,6 +311,14 @@ export interface WorkflowIrV1 {
edges: WorkflowIrEdge[];
}
/** Workflow-declared optional step backed by a workflow-step template.
* Execution-inert: consumed by create/edit UI to seed per-task
* `enabledWorkflowSteps`, never by the graph executor. Absent on legacy graphs. */
export interface WorkflowOptionalStep {
templateId: string;
defaultOn?: boolean;
}
/** A v2 workflow IR graph: v1 plus workflow-defined columns and node placement.
* Step-inversion adds optional `artifacts` (KTD-12) and `fields` (KTD-13)
* declarations — both additive; absent on legacy graphs. */
@@ -325,6 +333,9 @@ export interface WorkflowIrV2 {
/** Workflow-settings (U1, R1): typed setting declarations. Additive; absent on
* legacy graphs. Values persist per-`(workflowId, projectId)` (U2), not here. */
settings?: WorkflowSettingDefinition[];
/** Optional workflow-step templates tasks may independently enable/disable via
* `enabledWorkflowSteps`. Execution-inert; the graph executor ignores this facet. */
optionalSteps?: WorkflowOptionalStep[];
}
/** Either IR version. v1 graphs upgrade to v2 on parse (see parseWorkflowIr). */

View File

@@ -13,6 +13,7 @@ import type {
WorkflowFieldType,
WorkflowSettingDefinition,
WorkflowSettingType,
WorkflowOptionalStep,
} from "./workflow-ir-types.js";
import { getWorkflowExtensionRegistry } from "./workflow-extension-registry.js";
import type { WorkflowExtensionConfigField } from "./workflow-extension-types.js";
@@ -1070,6 +1071,29 @@ function validateSettings(settings: WorkflowSettingDefinition[] | undefined): vo
}
}
function validateOptionalSteps(optionalSteps: WorkflowOptionalStep[] | undefined): void {
if (optionalSteps === undefined) return;
if (!Array.isArray(optionalSteps)) {
throw new WorkflowIrError("Workflow IR optionalSteps must be an array");
}
for (const optionalStep of optionalSteps) {
if (!optionalStep || typeof optionalStep !== "object" || Array.isArray(optionalStep)) {
throw new WorkflowIrError("Workflow optional step must be an object");
}
if (typeof optionalStep.templateId !== "string" || optionalStep.templateId === "") {
throw new WorkflowIrError("Workflow optional step must have a non-empty templateId");
}
if (
optionalStep.defaultOn !== undefined &&
typeof optionalStep.defaultOn !== "boolean"
) {
throw new WorkflowIrError(
`Workflow optional step '${optionalStep.templateId}' defaultOn must be a boolean`,
);
}
}
}
function validateColumns(ir: WorkflowIrV2): void {
if (!Array.isArray(ir.columns)) {
throw new WorkflowIrError("Workflow IR v2 columns must be an array");
@@ -1231,6 +1255,7 @@ function validateV2(ir: WorkflowIrV2): void {
validateNotifyNodes(ir.nodes);
validateFields(ir.fields);
validateSettings(ir.settings);
validateOptionalSteps(ir.optionalSteps);
// Rework edges are legal intra-template (foreach, KTD-5) and — since U6
// generalized the bounded-rework mechanism to the top-level walk — for a
@@ -1338,12 +1363,13 @@ export function downgradeIrToV1IfPure(ir: WorkflowIr): WorkflowIr {
if (!V1_NODE_KINDS.has(node.kind)) return ir;
}
// Step-inversion declarations (artifacts/fields) and workflow settings (U1)
// are v2-only features.
// Step-inversion declarations (artifacts/fields), workflow settings (U1), and
// optional workflow-step declarations are v2-only features.
if (
(ir.artifacts && ir.artifacts.length > 0) ||
(ir.fields && ir.fields.length > 0) ||
(ir.settings && ir.settings.length > 0)
(ir.settings && ir.settings.length > 0) ||
(ir.optionalSteps && ir.optionalSteps.length > 0)
) {
return ir;
}

View File

@@ -0,0 +1,46 @@
import type { WorkflowIr } from "./workflow-ir-types.js";
import { WORKFLOW_STEP_TEMPLATES, type WorkflowStepTemplate } from "./types.js";
export interface ResolvedWorkflowOptionalStep {
templateId: string;
name: string;
description: string;
icon?: string;
phase: NonNullable<WorkflowStepTemplate["phase"]>;
defaultOn: boolean;
}
/**
* Resolve workflow-declared optional step template ids into display metadata.
*
* The declaration is intentionally execution-inert: it only advertises which
* template-backed workflow steps a task may toggle into `enabledWorkflowSteps`.
* Unknown template ids are skipped so stale/custom declarations never render
* blank UI rows or break workflow loading.
*/
export function resolveWorkflowOptionalSteps(
ir: WorkflowIr,
pluginTemplates: WorkflowStepTemplate[] = [],
): ResolvedWorkflowOptionalStep[] {
if (ir.version !== "v2" || !ir.optionalSteps?.length) return [];
const templates = new Map<string, WorkflowStepTemplate>();
for (const template of [...WORKFLOW_STEP_TEMPLATES, ...pluginTemplates]) {
templates.set(template.id, template);
}
const resolved: ResolvedWorkflowOptionalStep[] = [];
for (const optionalStep of ir.optionalSteps) {
const template = templates.get(optionalStep.templateId);
if (!template) continue;
resolved.push({
templateId: optionalStep.templateId,
name: template.name,
description: template.description,
icon: template.icon,
phase: template.phase ?? "pre-merge",
defaultOn: optionalStep.defaultOn ?? template.defaultOn ?? false,
});
}
return resolved;
}

View File

@@ -5111,6 +5111,16 @@ export function fetchWorkflow(id: string, projectId?: string): Promise<import("@
return api<import("@fusion/core").WorkflowDefinition>(withProjectId(`/workflows/${encodeURIComponent(id)}`, projectId));
}
/** Fetch resolved optional step declarations for a workflow. */
export function fetchWorkflowOptionalSteps(
workflowId: string,
projectId?: string,
): Promise<import("@fusion/core").ResolvedWorkflowOptionalStep[]> {
return api<import("@fusion/core").ResolvedWorkflowOptionalStep[]>(
withProjectId(`/workflows/${encodeURIComponent(workflowId)}/optional-steps`, projectId),
);
}
/** Create a workflow definition. */
export function createWorkflow(
input: import("@fusion/core").WorkflowDefinitionInput,

View File

@@ -153,6 +153,18 @@
margin-left: auto;
}
.inline-create-optional-steps {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-xs);
}
.inline-create-optional-step[aria-pressed="true"] {
border-color: var(--accent);
color: var(--accent);
}
.inline-create-hint {
font-size: 11px;
color: var(--text-dim);
@@ -302,6 +314,14 @@
font-size: 12px;
}
.inline-create-optional-steps {
width: 100%;
}
.inline-create-optional-step {
flex: 1 1 auto;
}
.inline-create-priority-select {
min-height: 36px;
}

View File

@@ -3,10 +3,10 @@ import { useState, useCallback, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import { Brain, Link, Lightbulb, ListTree, Zap, ChevronDown, ChevronUp, Bot, Maximize2, Minimize2, Server } from "lucide-react";
import { DEFAULT_TASK_PRIORITY, TASK_PRIORITIES, type Task, type TaskPriority, type Settings } from "@fusion/core";
import { DEFAULT_TASK_PRIORITY, TASK_PRIORITIES, type Task, type TaskPriority, type Settings, type ResolvedWorkflowOptionalStep } from "@fusion/core";
import { getErrorMessage } from "@fusion/core";
import type { ToastType } from "../hooks/useToast";
import { checkDuplicateTasks, fetchModels, uploadAttachment, fetchSettings, updateGlobalSettings, fetchAgents, selectTaskWorkflow, DuplicateCandidatesError } from "../api";
import { checkDuplicateTasks, fetchModels, uploadAttachment, fetchSettings, updateGlobalSettings, fetchAgents, selectTaskWorkflow, fetchWorkflowOptionalSteps, DuplicateCandidatesError } from "../api";
import type { CreateTaskInput, ModelInfo, Agent, NodeInfo, DuplicateMatch } from "../api";
import { useNodes } from "../hooks/useNodes";
import { ModelSelectionModal } from "./ModelSelectionModal";
@@ -110,7 +110,8 @@ export function InlineCreateCard({
const [validatorModelId, setValidatorModelId] = useState<string | undefined>(undefined);
const [planningProvider, setPlanningProvider] = useState<string | undefined>(undefined);
const [planningModelId, setPlanningModelId] = useState<string | undefined>(undefined);
const [browserVerification, setBrowserVerification] = useState(false);
const [optionalSteps, setOptionalSteps] = useState<ResolvedWorkflowOptionalStep[]>([]);
const [enabledOptionalStepIds, setEnabledOptionalStepIds] = useState<string[]>([]);
const [priority, setPriority] = useState<TaskPriority>(DEFAULT_TASK_PRIORITY);
const [modelsLoading, setModelsLoading] = useState(false);
const [modelsError, setModelsError] = useState<string | null>(null);
@@ -264,6 +265,37 @@ export function InlineCreateCard({
const hasValidatorOverride = Boolean(validatorProvider && validatorModelId);
const hasPlanningOverride = Boolean(planningProvider && planningModelId);
const selectedModelCount = Number(hasExecutorOverride) + Number(hasValidatorOverride) + Number(hasPlanningOverride);
const effectiveWorkflowId = selectedWorkflowId || settings?.defaultWorkflowId || "builtin:coding";
useEffect(() => {
let cancelled = false;
setOptionalSteps([]);
setEnabledOptionalStepIds([]);
fetchWorkflowOptionalSteps(effectiveWorkflowId, projectId)
.then((steps) => {
if (cancelled) return;
setOptionalSteps(steps);
setEnabledOptionalStepIds(steps.filter((step) => step.defaultOn).map((step) => step.templateId));
})
.catch(() => {
if (cancelled) return;
setOptionalSteps([]);
setEnabledOptionalStepIds([]);
});
return () => {
cancelled = true;
};
}, [effectiveWorkflowId, projectId]);
const toggleOptionalStep = useCallback((templateId: string) => {
setEnabledOptionalStepIds((prev) => (
prev.includes(templateId)
? prev.filter((id) => id !== templateId)
: [...prev, templateId]
));
}, []);
// Track focus-out for conditional cancel behavior and justResetRef cleanup.
useEffect(() => {
@@ -389,7 +421,7 @@ export function InlineCreateCard({
setValidatorModelId(undefined);
setPlanningProvider(undefined);
setPlanningModelId(undefined);
setBrowserVerification(false);
setEnabledOptionalStepIds([]);
setPriority(DEFAULT_TASK_PRIORITY);
setDependencies([]);
setSelectedAgentId(null);
@@ -400,6 +432,7 @@ export function InlineCreateCard({
setIsModelModalOpen(false);
setShowPresets(false);
setSelectedWorkflowId(null);
setEnabledOptionalStepIds([]);
addToast(`Created ${task.id}`, "success");
// Collapse and clear localStorage after successful task creation
@@ -442,7 +475,7 @@ export function InlineCreateCard({
validatorModelId: hasValidatorOverride ? validatorModelId : undefined,
planningModelProvider: hasPlanningOverride ? planningProvider : undefined,
planningModelId: hasPlanningOverride ? planningModelId : undefined,
enabledWorkflowSteps: browserVerification ? ["browser-verification"] : undefined,
enabledWorkflowSteps: enabledOptionalStepIds.length ? enabledOptionalStepIds : undefined,
priority,
nodeId,
};
@@ -459,7 +492,7 @@ export function InlineCreateCard({
}
await submitTask(input);
}, [description, submitting, dependencies, selectedAgentId, selectedPresetId, hasExecutorOverride, executorProvider, executorModelId, hasValidatorOverride, validatorProvider, validatorModelId, hasPlanningOverride, planningProvider, planningModelId, browserVerification, priority, nodeId, projectId, addToast, submitTask]);
}, [description, submitting, dependencies, selectedAgentId, selectedPresetId, hasExecutorOverride, executorProvider, executorModelId, hasValidatorOverride, validatorProvider, validatorModelId, hasPlanningOverride, planningProvider, planningModelId, enabledOptionalStepIds, priority, nodeId, projectId, addToast, submitTask]);
const handleDuplicateProceed = useCallback(async () => {
const matches = duplicateMatches;
@@ -671,7 +704,7 @@ export function InlineCreateCard({
setValidatorModelId(undefined);
setPlanningProvider(undefined);
setPlanningModelId(undefined);
setBrowserVerification(false);
setEnabledOptionalStepIds([]);
setSelectedPresetId(undefined);
setSelectedAgentId(null);
setNodeId(undefined);
@@ -699,7 +732,7 @@ export function InlineCreateCard({
setValidatorModelId(undefined);
setPlanningProvider(undefined);
setPlanningModelId(undefined);
setBrowserVerification(false);
setEnabledOptionalStepIds([]);
setSelectedPresetId(undefined);
setSelectedAgentId(null);
setNodeId(undefined);
@@ -1036,16 +1069,34 @@ export function InlineCreateCard({
)}
</div>
<button
type="button"
className="btn btn-sm"
data-testid="inline-create-browser-verification-toggle"
aria-pressed={browserVerification}
onClick={() => setBrowserVerification((prev) => !prev)}
title={t("inline.enableBrowserVerification", "Enable browser verification workflow step")}
>
{browserVerification ? t("inline.browserVerifyChecked", "Browser Verify ✓") : t("inline.browserVerify", "Browser Verify")}
</button>
{optionalSteps.length > 0 && (
<div
className="inline-create-optional-steps"
aria-label={t("inline.optionalWorkflowSteps", "Optional workflow steps")}
>
{optionalSteps.map((step) => {
const enabled = enabledOptionalStepIds.includes(step.templateId);
const testId = step.templateId === "browser-verification"
? "inline-create-browser-verification-toggle"
: `inline-create-optional-step-${step.templateId}`;
return (
<button
key={step.templateId}
type="button"
className="btn btn-sm inline-create-optional-step"
data-testid={testId}
aria-pressed={enabled}
onClick={() => toggleOptionalStep(step.templateId)}
title={t("inline.toggleOptionalWorkflowStep", "Toggle optional workflow step: {{name}}", { name: step.name })}
>
{enabled
? t("inline.optionalWorkflowStepChecked", "{{name}} ✓", { name: step.name })
: step.name}
</button>
);
})}
</div>
)}
<WorkflowSelector
value={selectedWorkflowId}

View File

@@ -6,9 +6,9 @@ import { Check, ChevronDown, ChevronRight, ChevronUp, Maximize2, Pencil, X } fro
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { ReactFlow, ReactFlowProvider } from "@xyflow/react";
import type { AgentLogEntry, Settings, Task, TaskDetail, WorkflowDefinition, WorkflowStep, WorkflowStepResult } from "@fusion/core";
import type { AgentLogEntry, Settings, Task, TaskDetail, WorkflowDefinition, WorkflowStep, WorkflowStepResult, ResolvedWorkflowOptionalStep } from "@fusion/core";
import { getErrorMessage, resolveTaskExecutionModel, resolveTaskPlanningModel, resolveTaskValidatorModel } from "@fusion/core";
import { approveTaskWorkflowCli, fetchWorkflow, fetchWorkflows, fetchWorkflowSteps, fetchTaskWorkflow, selectTaskWorkflow, submitTaskWorkflowInput } from "../api";
import { approveTaskWorkflowCli, fetchWorkflow, fetchWorkflows, fetchWorkflowSteps, fetchTaskWorkflow, fetchWorkflowOptionalSteps, selectTaskWorkflow, submitTaskWorkflowInput } from "../api";
import { WorkflowSelector } from "./WorkflowSelector";
import { useAgentLogs } from "../hooks/useAgentLogs";
import { ProviderIcon } from "./ProviderIcon";
@@ -323,6 +323,7 @@ export function WorkflowResultsTab({
const [submitted, setSubmitted] = useState(false);
const [expandedViewStepId, setExpandedViewStepId] = useState<string | null>(null);
const [allWorkflowSteps, setAllWorkflowSteps] = useState<WorkflowStep[]>([]);
const [optionalWorkflowSteps, setOptionalWorkflowSteps] = useState<ResolvedWorkflowOptionalStep[]>([]);
const [workflowDefinitions, setWorkflowDefinitions] = useState<WorkflowDefinition[]>([]);
const [isEditing, setIsEditing] = useState(false);
const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | null>(null);
@@ -435,16 +436,48 @@ export function WorkflowResultsTab({
};
}, [projectId]);
const effectiveOptionalStepsWorkflowId = selectedWorkflowId || "builtin:coding";
useEffect(() => {
let cancelled = false;
fetchWorkflowOptionalSteps(effectiveOptionalStepsWorkflowId, projectId)
.then((steps) => {
if (!cancelled) setOptionalWorkflowSteps(steps);
})
.catch(() => {
if (!cancelled) setOptionalWorkflowSteps([]);
});
return () => {
cancelled = true;
};
}, [effectiveOptionalStepsWorkflowId, projectId]);
const selectedWorkflowSteps = enabledWorkflowSteps ?? [];
const workflowStepOptions = useMemo<WorkflowStepOption[]>(() => {
return allWorkflowSteps.map((step) => ({
const options: WorkflowStepOption[] = allWorkflowSteps.map((step) => ({
id: step.id,
name: step.name,
description: step.description,
phase: (step.phase || "pre-merge") as "pre-merge" | "post-merge",
}));
}, [allWorkflowSteps]);
const seen = new Set<string>();
for (const step of allWorkflowSteps) {
seen.add(step.id);
if (step.templateId) seen.add(step.templateId);
}
for (const step of optionalWorkflowSteps) {
if (seen.has(step.templateId)) continue;
seen.add(step.templateId);
options.push({
id: step.templateId,
name: step.name,
description: step.description,
phase: step.phase,
});
}
return options;
}, [allWorkflowSteps, optionalWorkflowSteps]);
const workflowStepLookup = useMemo(() => {
return new Map(workflowStepOptions.map((step) => [step.id, step]));
@@ -840,7 +873,8 @@ export function WorkflowResultsTab({
</button>
) : null;
const showConfiguredStepsState = !loading && !hasResults && hasConfiguredSteps;
const hasEditableStepOptions = workflowStepOptions.length > 0;
const showConfiguredStepsState = !loading && !hasResults && (hasConfiguredSteps || (canEdit && hasEditableStepOptions));
const showEditHeaderForResults = canEdit && hasResults;
const isAwaitingInput = taskStatus === "awaiting-user-input";

View File

@@ -3,7 +3,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
import { InlineCreateCard } from "../InlineCreateCard";
import type { Task, Column } from "@fusion/core";
import { fetchModels, fetchSettings, fetchAgents, checkDuplicateTasks, fetchWorkflows } from "../../api";
import { fetchModels, fetchSettings, fetchAgents, checkDuplicateTasks, fetchWorkflows, fetchWorkflowOptionalSteps } from "../../api";
import { useNodes } from "../../hooks/useNodes";
import type { ModelInfo } from "../../api";
import { scopedKey } from "../../utils/projectStorage";
@@ -127,6 +127,16 @@ vi.mock("../../api", () => ({
updateGlobalSettings: vi.fn(),
fetchAgents: vi.fn().mockResolvedValue([]),
selectTaskWorkflow: vi.fn().mockResolvedValue({ workflowId: null, enabledWorkflowSteps: [] }),
fetchWorkflowOptionalSteps: vi.fn().mockResolvedValue([
{
templateId: "browser-verification",
name: "Browser Verification",
description: "Verify web application functionality using browser automation",
icon: "globe",
phase: "pre-merge",
defaultOn: false,
},
]),
fetchWorkflows: vi.fn().mockResolvedValue([]),
fetchProjectDefaultWorkflow: vi.fn().mockResolvedValue({ workflowId: null }),
setProjectDefaultWorkflow: vi.fn().mockResolvedValue({ workflowId: null }),
@@ -239,6 +249,16 @@ beforeEach(() => {
{ id: "wf-a", name: "Workflow A" },
{ id: "wf-b", name: "Workflow B" },
]);
vi.mocked(fetchWorkflowOptionalSteps).mockResolvedValue([
{
templateId: "browser-verification",
name: "Browser Verification",
description: "Verify web application functionality using browser automation",
icon: "globe",
phase: "pre-merge",
defaultOn: false,
},
]);
});
describe("InlineCreateCard textarea width (FN-1608)", () => {
@@ -1020,6 +1040,18 @@ describe("InlineCreateCard button visibility when collapsed", () => {
expect(document.getElementById("inline-create-controls")).toBeTruthy();
});
it("renders no optional-step shell when workflow has no optional steps", async () => {
vi.mocked(fetchWorkflowOptionalSteps).mockResolvedValueOnce([]);
renderCard([]);
expandCard();
await waitFor(() => {
expect(fetchWorkflowOptionalSteps).toHaveBeenCalled();
});
expect(screen.queryByTestId("inline-create-browser-verification-toggle")).not.toBeInTheDocument();
expect(document.querySelector(".inline-create-optional-steps")).toBeNull();
});
it("submits with enabledWorkflowSteps undefined", async () => {
const mockOnSubmit = vi.fn().mockResolvedValue(createMockTask());
renderCard([], { onSubmit: mockOnSubmit });
@@ -1047,7 +1079,12 @@ describe("InlineCreateCard button visibility when collapsed", () => {
target: { value: "Verify login flow in browser" },
});
fireEvent.click(screen.getByTestId("inline-create-browser-verification-toggle"));
const toggle = await screen.findByTestId("inline-create-browser-verification-toggle");
expect(toggle).toHaveTextContent("Browser Verification");
expect(toggle).toHaveAttribute("aria-pressed", "false");
fireEvent.click(toggle);
expect(toggle).toHaveAttribute("aria-pressed", "true");
fireEvent.click(screen.getByTestId("save-button"));
await waitFor(() => {

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { render, screen, fireEvent, waitFor, within } from "@testing-library/react";
import { WorkflowResultsTab } from "../WorkflowResultsTab";
import { fetchWorkflow, fetchWorkflows, fetchWorkflowSteps } from "../../api";
import { fetchWorkflow, fetchWorkflows, fetchWorkflowSteps, fetchWorkflowOptionalSteps } from "../../api";
import { useAgentLogs } from "../../hooks/useAgentLogs";
import { loadAllAppCss, loadAllAppCssBaseOnly } from "../../test/cssFixture";
import type { AgentLogEntry, Settings, Task, WorkflowDefinition, WorkflowStep, WorkflowStepResult } from "@fusion/core";
@@ -12,6 +12,7 @@ vi.mock("../../api", () => ({
selectTaskWorkflow: vi.fn().mockResolvedValue({ workflowId: "WF-001", enabledWorkflowSteps: [] }),
fetchWorkflows: vi.fn().mockResolvedValue([]),
fetchWorkflow: vi.fn(),
fetchWorkflowOptionalSteps: vi.fn(),
submitTaskWorkflowInput: vi.fn().mockResolvedValue({ ok: true }),
approveTaskWorkflowCli: vi.fn().mockResolvedValue({ approved: "ok" }),
}));
@@ -32,6 +33,7 @@ vi.mock("../../hooks/useAgentLogs", () => ({
const mockedFetchWorkflowSteps = vi.mocked(fetchWorkflowSteps);
const mockedFetchWorkflow = vi.mocked(fetchWorkflow);
const mockedFetchWorkflows = vi.mocked(fetchWorkflows);
const mockedFetchWorkflowOptionalSteps = vi.mocked(fetchWorkflowOptionalSteps);
const mockedUseAgentLogs = vi.mocked(useAgentLogs);
describe("WorkflowResultsTab", () => {
@@ -133,6 +135,17 @@ describe("WorkflowResultsTab", () => {
mockedFetchWorkflow.mockResolvedValue(selectedWorkflow);
mockedFetchWorkflows.mockReset();
mockedFetchWorkflows.mockResolvedValue([selectedWorkflow]);
mockedFetchWorkflowOptionalSteps.mockReset();
mockedFetchWorkflowOptionalSteps.mockResolvedValue([
{
templateId: "browser-verification",
name: "Browser Verification",
description: "Verify web application functionality using browser automation",
icon: "globe",
phase: "pre-merge",
defaultOn: false,
},
]);
mockedUseAgentLogs.mockReset();
mockedUseAgentLogs.mockReturnValue({
entries: [],
@@ -1019,6 +1032,49 @@ describe("WorkflowResultsTab", () => {
expect(within(editor).getAllByText("Browser Verification")).toHaveLength(1);
});
it("renders workflow-declared optional steps when not materialized", async () => {
mockedFetchWorkflowSteps.mockResolvedValueOnce(mockWorkflowSteps.filter((step) => step.id !== "WS-103"));
const onWorkflowStepsChange = vi.fn();
render(
<WorkflowResultsTab
taskId="FN-001"
results={[]}
canEdit
enabledWorkflowSteps={[]}
onWorkflowStepsChange={onWorkflowStepsChange}
/>,
);
fireEvent.click(await screen.findByTestId("workflow-steps-edit-toggle"));
const checkbox = await screen.findByTestId("workflow-step-checkbox-browser-verification");
expect(within(checkbox).getByText("Browser Verification")).toBeInTheDocument();
fireEvent.click(within(checkbox).getByRole("checkbox"));
expect(onWorkflowStepsChange).toHaveBeenCalledWith(["browser-verification"]);
});
it("disabling a workflow-declared optional step removes its template id", async () => {
mockedFetchWorkflowSteps.mockResolvedValueOnce(mockWorkflowSteps.filter((step) => step.id !== "WS-103"));
const onWorkflowStepsChange = vi.fn();
render(
<WorkflowResultsTab
taskId="FN-001"
results={[]}
canEdit
enabledWorkflowSteps={["WS-101", "browser-verification"]}
onWorkflowStepsChange={onWorkflowStepsChange}
/>,
);
fireEvent.click(await screen.findByTestId("workflow-steps-edit-toggle"));
const checkbox = await screen.findByTestId("workflow-step-checkbox-browser-verification");
fireEvent.click(within(checkbox).getByRole("checkbox"));
expect(onWorkflowStepsChange).toHaveBeenCalledWith(["WS-101"]);
});
it("fetches workflow step definitions when canEdit and projectId are provided", async () => {
render(
<WorkflowResultsTab

View File

@@ -230,6 +230,28 @@ describe("workflow routes (U4)", () => {
expect(list.some((w) => isBuiltinWorkflowId(w.id))).toBe(true);
});
it("GET /workflows/:id/optional-steps resolves declared optional steps", async () => {
const builtin = await get("/api/workflows/builtin%3Acoding/optional-steps");
expect(builtin.status).toBe(200);
expect(builtin.body).toEqual([
expect.objectContaining({
templateId: "browser-verification",
name: "Browser Verification",
icon: "globe",
defaultOn: false,
}),
]);
const custom = await post("/api/workflows", { name: "A", ir: linearIr() });
const customId = (custom.body as { id: string }).id;
const customSteps = await get(`/api/workflows/${customId}/optional-steps`);
expect(customSteps.status).toBe(200);
expect(customSteps.body).toEqual([]);
const missing = await get("/api/workflows/WF-404/optional-steps");
expect(missing.status).toBe(404);
});
it("GET /traits returns the registry trait catalog (built-ins, with flags + schema)", async () => {
const res = await get("/api/traits");
expect(res.status).toBe(200);

View File

@@ -1,5 +1,5 @@
import type { WorkflowDefinition, WorkflowDefinitionKind, WorkflowIr, WorkflowIrNode, WorkflowSettingDefinition, TaskStore } from "@fusion/core";
import { ColumnTraitValidationError, OccupiedColumnsError, InvalidRehomeTargetError, WorkflowCompileError, WorkflowIrError, ColumnAgentBindingError, WorkflowSettingRejectionError, SCHEMA_VERSION, assertColumnTraitsValid, compileWorkflowToSteps, layoutForIr, listTraits, listStepParsers, parseWorkflowIr, resolvePlanningSettingsModel, stripApprovalBypassFlags, resolveWorkflowIrById, resolveEffectiveSettingValues, findOrphanedSettingValues, isBuiltinWorkflowId, BUILTIN_WORKFLOW_SETTINGS, AgentStore, validateColumnAgentBindings } from "@fusion/core";
import { ColumnTraitValidationError, OccupiedColumnsError, InvalidRehomeTargetError, WorkflowCompileError, WorkflowIrError, ColumnAgentBindingError, WorkflowSettingRejectionError, SCHEMA_VERSION, assertColumnTraitsValid, compileWorkflowToSteps, layoutForIr, listTraits, listStepParsers, parseWorkflowIr, resolvePlanningSettingsModel, stripApprovalBypassFlags, resolveWorkflowIrById, resolveEffectiveSettingValues, findOrphanedSettingValues, isBuiltinWorkflowId, BUILTIN_WORKFLOW_SETTINGS, AgentStore, validateColumnAgentBindings, resolveWorkflowOptionalSteps } from "@fusion/core";
import { createFnAgent as engineCreateFnAgent, validateCodeNodeSources } from "@fusion/engine";
import { ApiError, badRequest, conflict, notFound, rateLimited } from "../api-error.js";
import { emitWorkflowSseEvent } from "../sse.js";
@@ -312,6 +312,20 @@ export function registerWorkflowRoutes(ctx: ApiRoutesContext): void {
}
});
// GET /api/workflows/:id/optional-steps — resolved optional step metadata.
router.get("/workflows/:id/optional-steps", async (req, res) => {
try {
const { store } = await getProjectContext(req);
const workflowId = req.params.id;
await assertWorkflowExists(store, workflowId);
const ir = await resolveWorkflowIrById(store, workflowId);
res.json(resolveWorkflowOptionalSteps(ir));
} catch (err: unknown) {
if (err instanceof ApiError) throw err;
rethrowAsApiError(err);
}
});
// GET /api/workflows/:id
router.get("/workflows/:id", async (req, res) => {
try {

View File

@@ -2509,8 +2509,6 @@
"inline": {
"agent": "Agent",
"breakDownSubtasks": "Break down into AI-generated subtasks",
"browserVerify": "Browser Verify",
"browserVerifyChecked": "Browser Verify ✓",
"clearSelection": "Clear selection",
"collapse": "Collapse",
"collapseDescription": "Collapse description",
@@ -2519,7 +2517,6 @@
"custom": "Custom",
"deps": "Deps",
"editingDescription": "Editing Description",
"enableBrowserVerification": "Enable browser verification workflow step",
"enterDescriptionFirst": "Enter a description first",
"expand": "Expand",
"expandDescription": "Expand description",
@@ -2533,6 +2530,8 @@
"node": "Node",
"noExistingTasks": "No existing tasks",
"openPlanningMode": "Open planning mode with current description",
"optionalWorkflowStepChecked": "{{name}} ✓",
"optionalWorkflowSteps": "Optional workflow steps",
"plan": "Plan",
"preset": "Preset",
"priority": "Priority",
@@ -2544,6 +2543,7 @@
"selectAgent": "Select agent",
"selectExecutionNode": "Select execution node",
"subtask": "Subtask",
"toggleOptionalWorkflowStep": "Toggle optional workflow step: {{name}}",
"useDefault": "Use default",
"whatNeedsToBeDone": "What needs to be done?"
},

View File

@@ -2508,8 +2508,6 @@
"inline": {
"agent": "Agente",
"breakDownSubtasks": "Desglosar en subtareas generadas por IA",
"browserVerify": "Verificación del navegador",
"browserVerifyChecked": "Verificación del navegador ✓",
"clearSelection": "Borrar selección",
"collapse": "Contraer",
"collapseDescription": "Contraer descripción",
@@ -2518,7 +2516,6 @@
"custom": "Personalizado",
"deps": "Dependencias",
"editingDescription": "Descripción de edición",
"enableBrowserVerification": "Habilitar paso de flujo de verificación del navegador",
"enterDescriptionFirst": "Ingrese una descripción primero",
"expand": "Expandir",
"expandDescription": "Expandir descripción",

View File

@@ -2508,8 +2508,6 @@
"inline": {
"agent": "Agent",
"breakDownSubtasks": "Décomposer en sous-tâches générées par l'IA",
"browserVerify": "Vérification du navigateur",
"browserVerifyChecked": "Vérification du navigateur ✓",
"clearSelection": "Effacer la sélection",
"collapse": "Réduire",
"collapseDescription": "Décrire la description",
@@ -2518,7 +2516,6 @@
"custom": "Personnalisé",
"deps": "Dépendances",
"editingDescription": "Description d'édition",
"enableBrowserVerification": "Activer l'étape de flux de vérification du navigateur",
"enterDescriptionFirst": "Entrez d'abord une description",
"expand": "Développer",
"expandDescription": "Développer la description",

View File

@@ -2508,8 +2508,6 @@
"inline": {
"agent": "에이전트",
"breakDownSubtasks": "AI 생성 하위 작업으로 분해",
"browserVerify": "브라우저 검증",
"browserVerifyChecked": "브라우저 검증 ✓",
"clearSelection": "선택 취소",
"collapse": "접기",
"collapseDescription": "설명 접기",
@@ -2518,7 +2516,6 @@
"custom": "사용자 정의",
"deps": "의존성",
"editingDescription": "설명 편집 중",
"enableBrowserVerification": "브라우저 검증 워크플로 단계 활성화",
"enterDescriptionFirst": "먼저 설명을 입력하세요.",
"expand": "펼치기",
"expandDescription": "설명 펼치기",

View File

@@ -2508,8 +2508,6 @@
"inline": {
"agent": "代理",
"breakDownSubtasks": "分解为 AI 生成的子任务",
"browserVerify": "浏览器验证",
"browserVerifyChecked": "浏览器验证 ✓",
"clearSelection": "清除选择",
"collapse": "折叠",
"collapseDescription": "折叠描述",
@@ -2518,7 +2516,6 @@
"custom": "自定义",
"deps": "依赖",
"editingDescription": "编辑描述",
"enableBrowserVerification": "启用浏览器验证工作流步骤",
"enterDescriptionFirst": "先输入描述",
"expand": "展开",
"expandDescription": "展开描述",

View File

@@ -2508,8 +2508,6 @@
"inline": {
"agent": "代理",
"breakDownSubtasks": "分解為 AI 生成的子工作",
"browserVerify": "瀏覽器驗證",
"browserVerifyChecked": "瀏覽器驗證 ✓",
"clearSelection": "清除選擇",
"collapse": "折疊",
"collapseDescription": "折疊說明",
@@ -2518,7 +2516,6 @@
"custom": "自訂",
"deps": "相依性",
"editingDescription": "編輯說明",
"enableBrowserVerification": "啟用瀏覽器驗證工作流程步驟",
"enterDescriptionFirst": "先輸入說明",
"expand": "展開",
"expandDescription": "展開說明",

View File

@@ -2501,8 +2501,6 @@ export default interface Resources {
"inline": {
"agent": "Agent",
"breakDownSubtasks": "Break down into AI-generated subtasks",
"browserVerify": "Browser Verify",
"browserVerifyChecked": "Browser Verify ✓",
"clearSelection": "Clear selection",
"collapse": "Collapse",
"collapseDescription": "Collapse description",
@@ -2511,7 +2509,6 @@ export default interface Resources {
"custom": "Custom",
"deps": "Deps",
"editingDescription": "Editing Description",
"enableBrowserVerification": "Enable browser verification workflow step",
"enterDescriptionFirst": "Enter a description first",
"expand": "Expand",
"expandDescription": "Expand description",
@@ -2525,6 +2522,8 @@ export default interface Resources {
"noExistingTasks": "No existing tasks",
"node": "Node",
"openPlanningMode": "Open planning mode with current description",
"optionalWorkflowStepChecked": "{{name}} ✓",
"optionalWorkflowSteps": "Optional workflow steps",
"plan": "Plan",
"preset": "Preset",
"priority": "Priority",
@@ -2536,6 +2535,7 @@ export default interface Resources {
"selectAgent": "Select agent",
"selectExecutionNode": "Select execution node",
"subtask": "Subtask",
"toggleOptionalWorkflowStep": "Toggle optional workflow step: {{name}}",
"useDefault": "Use default",
"whatNeedsToBeDone": "What needs to be done?"
},