From bd87ce7d7e91a55b902f906abcbc1df01b52e685 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 12 Jun 2026 17:57:19 -0700 Subject: [PATCH] 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 --- .changeset/fn-6304-optional-workflow-steps.md | 5 ++ docs/task-management.md | 2 +- docs/workflow-steps.md | 8 ++ .../core/src/__tests__/workflow-ir.test.ts | 45 ++++++++++ .../__tests__/workflow-optional-steps.test.ts | 88 +++++++++++++++++++ .../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 +++ .../app/components/InlineCreateCard.css | 20 +++++ .../app/components/InlineCreateCard.tsx | 87 ++++++++++++++---- .../app/components/WorkflowResultsTab.tsx | 44 ++++++++-- .../__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(-) create mode 100644 .changeset/fn-6304-optional-workflow-steps.md create mode 100644 packages/core/src/__tests__/workflow-optional-steps.test.ts create mode 100644 packages/core/src/workflow-optional-steps.ts diff --git a/.changeset/fn-6304-optional-workflow-steps.md b/.changeset/fn-6304-optional-workflow-steps.md new file mode 100644 index 0000000000..4a965f4341 --- /dev/null +++ b/.changeset/fn-6304-optional-workflow-steps.md @@ -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. diff --git a/docs/task-management.md b/docs/task-management.md index bf30bdbf28..a2646ba2a9 100644 --- a/docs/task-management.md +++ b/docs/task-management.md @@ -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 diff --git a/docs/workflow-steps.md b/docs/workflow-steps.md index 1e4bd5bce9..11cbabeb58 100644 --- a/docs/workflow-steps.md +++ b/docs/workflow-steps.md @@ -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. diff --git a/packages/core/src/__tests__/workflow-ir.test.ts b/packages/core/src/__tests__/workflow-ir.test.ts index 599b06c111..81e4c39dba 100644 --- a/packages/core/src/__tests__/workflow-ir.test.ts +++ b/packages/core/src/__tests__/workflow-ir.test.ts @@ -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", diff --git a/packages/core/src/__tests__/workflow-optional-steps.test.ts b/packages/core/src/__tests__/workflow-optional-steps.test.ts new file mode 100644 index 0000000000..93bb6661bf --- /dev/null +++ b/packages/core/src/__tests__/workflow-optional-steps.test.ts @@ -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, + }); + }); +}); diff --git a/packages/core/src/builtin-coding-workflow-ir.ts b/packages/core/src/builtin-coding-workflow-ir.ts index 4eaa7ab215..f663d6b7fd 100644 --- a/packages/core/src/builtin-coding-workflow-ir.ts +++ b/packages/core/src/builtin-coding-workflow-ir.ts @@ -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); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1515e727c7..56f050a569 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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 { diff --git a/packages/core/src/store.ts b/packages/core/src/store.ts index 728d8d2a96..24c1a0e1f9 100644 --- a/packages/core/src/store.ts +++ b/packages/core/src/store.ts @@ -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 }; } diff --git a/packages/core/src/workflow-compiler.ts b/packages/core/src/workflow-compiler.ts index 0ebad8ab48..5d617d0a7b 100644 --- a/packages/core/src/workflow-compiler.ts +++ b/packages/core/src/workflow-compiler.ts @@ -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([ + "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") { diff --git a/packages/core/src/workflow-ir-types.ts b/packages/core/src/workflow-ir-types.ts index 5528fce848..f2d5c90e1d 100644 --- a/packages/core/src/workflow-ir-types.ts +++ b/packages/core/src/workflow-ir-types.ts @@ -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). */ diff --git a/packages/core/src/workflow-ir.ts b/packages/core/src/workflow-ir.ts index a0cbdf400d..ff13a996b3 100644 --- a/packages/core/src/workflow-ir.ts +++ b/packages/core/src/workflow-ir.ts @@ -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; } diff --git a/packages/core/src/workflow-optional-steps.ts b/packages/core/src/workflow-optional-steps.ts new file mode 100644 index 0000000000..6b1c714ee2 --- /dev/null +++ b/packages/core/src/workflow-optional-steps.ts @@ -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; + 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(); + 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; +} diff --git a/packages/dashboard/app/api/legacy.ts b/packages/dashboard/app/api/legacy.ts index 2cbd37729a..f8d98c8740 100644 --- a/packages/dashboard/app/api/legacy.ts +++ b/packages/dashboard/app/api/legacy.ts @@ -5111,6 +5111,16 @@ export function fetchWorkflow(id: string, projectId?: string): Promise(withProjectId(`/workflows/${encodeURIComponent(id)}`, projectId)); } +/** Fetch resolved optional step declarations for a workflow. */ +export function fetchWorkflowOptionalSteps( + workflowId: string, + projectId?: string, +): Promise { + return api( + withProjectId(`/workflows/${encodeURIComponent(workflowId)}/optional-steps`, projectId), + ); +} + /** Create a workflow definition. */ export function createWorkflow( input: import("@fusion/core").WorkflowDefinitionInput, diff --git a/packages/dashboard/app/components/InlineCreateCard.css b/packages/dashboard/app/components/InlineCreateCard.css index 505d845d34..6d361c59f0 100644 --- a/packages/dashboard/app/components/InlineCreateCard.css +++ b/packages/dashboard/app/components/InlineCreateCard.css @@ -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; } diff --git a/packages/dashboard/app/components/InlineCreateCard.tsx b/packages/dashboard/app/components/InlineCreateCard.tsx index e42ac4bbe4..82d689cd12 100644 --- a/packages/dashboard/app/components/InlineCreateCard.tsx +++ b/packages/dashboard/app/components/InlineCreateCard.tsx @@ -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(undefined); const [planningProvider, setPlanningProvider] = useState(undefined); const [planningModelId, setPlanningModelId] = useState(undefined); - const [browserVerification, setBrowserVerification] = useState(false); + const [optionalSteps, setOptionalSteps] = useState([]); + const [enabledOptionalStepIds, setEnabledOptionalStepIds] = useState([]); const [priority, setPriority] = useState(DEFAULT_TASK_PRIORITY); const [modelsLoading, setModelsLoading] = useState(false); const [modelsError, setModelsError] = useState(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({ )} - + {optionalSteps.length > 0 && ( +
+ {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 ( + + ); + })} +
+ )} (null); const [allWorkflowSteps, setAllWorkflowSteps] = useState([]); + const [optionalWorkflowSteps, setOptionalWorkflowSteps] = useState([]); const [workflowDefinitions, setWorkflowDefinitions] = useState([]); const [isEditing, setIsEditing] = useState(false); const [selectedWorkflowId, setSelectedWorkflowId] = useState(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(() => { - 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(); + 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({ ) : 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"; diff --git a/packages/dashboard/app/components/__tests__/InlineCreateCard.test.tsx b/packages/dashboard/app/components/__tests__/InlineCreateCard.test.tsx index ad88ab8b02..d2d823997a 100644 --- a/packages/dashboard/app/components/__tests__/InlineCreateCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/InlineCreateCard.test.tsx @@ -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(() => { diff --git a/packages/dashboard/app/components/__tests__/WorkflowResultsTab.test.tsx b/packages/dashboard/app/components/__tests__/WorkflowResultsTab.test.tsx index 0829d52a72..9a074c6aaf 100644 --- a/packages/dashboard/app/components/__tests__/WorkflowResultsTab.test.tsx +++ b/packages/dashboard/app/components/__tests__/WorkflowResultsTab.test.tsx @@ -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( + , + ); + + 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( + , + ); + + 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( { 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); diff --git a/packages/dashboard/src/routes/register-workflow-routes.ts b/packages/dashboard/src/routes/register-workflow-routes.ts index 1355473083..871447bfd6 100644 --- a/packages/dashboard/src/routes/register-workflow-routes.ts +++ b/packages/dashboard/src/routes/register-workflow-routes.ts @@ -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 { diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json index 68541b2ad0..49899baf14 100644 --- a/packages/i18n/locales/en/app.json +++ b/packages/i18n/locales/en/app.json @@ -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?" }, diff --git a/packages/i18n/locales/es/app.json b/packages/i18n/locales/es/app.json index 1639325cb3..3492c793ea 100644 --- a/packages/i18n/locales/es/app.json +++ b/packages/i18n/locales/es/app.json @@ -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", diff --git a/packages/i18n/locales/fr/app.json b/packages/i18n/locales/fr/app.json index 9fd8329e99..1195ed1917 100644 --- a/packages/i18n/locales/fr/app.json +++ b/packages/i18n/locales/fr/app.json @@ -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", diff --git a/packages/i18n/locales/ko/app.json b/packages/i18n/locales/ko/app.json index 8c2d756e26..18892afaed 100644 --- a/packages/i18n/locales/ko/app.json +++ b/packages/i18n/locales/ko/app.json @@ -2508,8 +2508,6 @@ "inline": { "agent": "에이전트", "breakDownSubtasks": "AI 생성 하위 작업으로 분해", - "browserVerify": "브라우저 검증", - "browserVerifyChecked": "브라우저 검증 ✓", "clearSelection": "선택 취소", "collapse": "접기", "collapseDescription": "설명 접기", @@ -2518,7 +2516,6 @@ "custom": "사용자 정의", "deps": "의존성", "editingDescription": "설명 편집 중", - "enableBrowserVerification": "브라우저 검증 워크플로 단계 활성화", "enterDescriptionFirst": "먼저 설명을 입력하세요.", "expand": "펼치기", "expandDescription": "설명 펼치기", diff --git a/packages/i18n/locales/zh-CN/app.json b/packages/i18n/locales/zh-CN/app.json index a033625f5d..4dc490a62b 100644 --- a/packages/i18n/locales/zh-CN/app.json +++ b/packages/i18n/locales/zh-CN/app.json @@ -2508,8 +2508,6 @@ "inline": { "agent": "代理", "breakDownSubtasks": "分解为 AI 生成的子任务", - "browserVerify": "浏览器验证", - "browserVerifyChecked": "浏览器验证 ✓", "clearSelection": "清除选择", "collapse": "折叠", "collapseDescription": "折叠描述", @@ -2518,7 +2516,6 @@ "custom": "自定义", "deps": "依赖", "editingDescription": "编辑描述", - "enableBrowserVerification": "启用浏览器验证工作流步骤", "enterDescriptionFirst": "先输入描述", "expand": "展开", "expandDescription": "展开描述", diff --git a/packages/i18n/locales/zh-TW/app.json b/packages/i18n/locales/zh-TW/app.json index 7a9241c9aa..f008ffbc41 100644 --- a/packages/i18n/locales/zh-TW/app.json +++ b/packages/i18n/locales/zh-TW/app.json @@ -2508,8 +2508,6 @@ "inline": { "agent": "代理", "breakDownSubtasks": "分解為 AI 生成的子工作", - "browserVerify": "瀏覽器驗證", - "browserVerifyChecked": "瀏覽器驗證 ✓", "clearSelection": "清除選擇", "collapse": "折疊", "collapseDescription": "折疊說明", @@ -2518,7 +2516,6 @@ "custom": "自訂", "deps": "相依性", "editingDescription": "編輯說明", - "enableBrowserVerification": "啟用瀏覽器驗證工作流程步驟", "enterDescriptionFirst": "先輸入說明", "expand": "展開", "expandDescription": "展開說明", diff --git a/packages/i18n/src/resources.d.ts b/packages/i18n/src/resources.d.ts index 307209af43..a05b41d398 100644 --- a/packages/i18n/src/resources.d.ts +++ b/packages/i18n/src/resources.d.ts @@ -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?" },