diff --git a/packages/dashboard/app/api/legacy.ts b/packages/dashboard/app/api/legacy.ts index 8c4b40e8a6..ee6534266a 100644 --- a/packages/dashboard/app/api/legacy.ts +++ b/packages/dashboard/app/api/legacy.ts @@ -83,6 +83,11 @@ import type { WorkflowFieldType, WorkflowFieldOption, WorkflowFieldRender, + WorkflowSettingDefinition, + WorkflowSettingType, + WorkflowSettingOption, + WorkflowSettingRender, + WorkflowSettingRejection, } from "@fusion/core"; import type { PlanningQuestion, PlanningSummary } from "@fusion/core"; import type { GithubIssueAction, ScheduledTask, ScheduledTaskCreateInput, ScheduledTaskUpdateInput, AutomationRunResult, Routine, RoutineCreateInput, RoutineUpdateInput, RoutineExecutionResult } from "@fusion/core"; @@ -560,6 +565,10 @@ export interface BoardWorkflowColumn { // are re-exported from @fusion/core above (KTD-13/14). export type { WorkflowFieldDefinition, WorkflowFieldType, WorkflowFieldOption, WorkflowFieldRender }; +// Workflow-settings (U6/KTD-1) declaration types re-exported from @fusion/core so +// the WorkflowSettingsPanel imports them from `../api` like the field types. +export type { WorkflowSettingDefinition, WorkflowSettingType, WorkflowSettingOption, WorkflowSettingRender, WorkflowSettingRejection }; + export interface BoardWorkflowDefinition { id: string; name: string; @@ -5101,6 +5110,46 @@ export function deleteWorkflow(id: string, projectId?: string): Promise { return api(withProjectId(`/workflows/${encodeURIComponent(id)}`, projectId), { method: "DELETE" }); } +/** The per-`(workflowId, project)` setting-value payload returned by the + * workflow setting-value endpoints (U6/R5): the raw `stored` map, the + * `effective` map (stored ?? declaration default, drop-on-orphan), and the + * `orphaned` stored entries that no longer validate against the declarations. */ +export interface WorkflowSettingValuesPayload { + stored: Record; + effective: Record; + orphaned: Array<{ id: string; value: unknown }>; +} + +/** Read the setting VALUES (stored/effective/orphaned) for a workflow in the + * current project context (U6). The project is bound server-side to the + * scoped store. */ +export function fetchWorkflowSettingValues( + id: string, + projectId?: string, +): Promise { + return api( + withProjectId(`/workflows/${encodeURIComponent(id)}/setting-values`, projectId), + ); +} + +/** Write setting VALUES for a workflow in the current project context (U6). The + * `values` map is validated against the named workflow's declarations; a `null` + * value deletes that key. A typed rejection surfaces as an ApiRequestError with + * `status: 400` and `details.rejections: WorkflowSettingRejection[]`. */ +export function updateWorkflowSettingValues( + id: string, + values: Record, + projectId?: string, +): Promise { + return api( + withProjectId(`/workflows/${encodeURIComponent(id)}/setting-values`, projectId), + { + method: "PATCH", + body: JSON.stringify({ values }), + }, + ); +} + /** Preview the compiled steps for a workflow. Rejects (422) for non-linear graphs. */ export function compileWorkflow(id: string, projectId?: string): Promise<{ steps: WorkflowStepInput[] }> { return api<{ steps: WorkflowStepInput[] }>(withProjectId(`/workflows/${encodeURIComponent(id)}/compile`, projectId), { diff --git a/packages/dashboard/app/components/WorkflowNodeEditor.tsx b/packages/dashboard/app/components/WorkflowNodeEditor.tsx index 2f97816815..e2568dc48a 100644 --- a/packages/dashboard/app/components/WorkflowNodeEditor.tsx +++ b/packages/dashboard/app/components/WorkflowNodeEditor.tsx @@ -42,6 +42,7 @@ import { emptyWorkflowLayout, columnsOf, fieldsOf, + settingsOf, columnsToBandNodes, strictColumnForY, validateColumnsClient, @@ -57,7 +58,8 @@ import { import { fetchTraits, fetchStepParsers, type TraitCatalogEntry } from "../api"; import { WorkflowColumnPanel } from "./WorkflowColumnPanel"; import { WorkflowFieldsPanel } from "./WorkflowFieldsPanel"; -import type { WorkflowFieldDefinition } from "../api"; +import { WorkflowSettingsPanel } from "./WorkflowSettingsPanel"; +import type { WorkflowFieldDefinition, WorkflowSettingDefinition } from "../api"; import { CustomModelDropdown } from "./CustomModelDropdown"; type ExecutorKind = "model" | "agent" | "skill" | "cli"; @@ -85,6 +87,10 @@ interface WorkflowNodeEditorProps { onClose: () => void; addToast: (message: string, type?: ToastType) => void; projectId?: string; + /** When "settings" the editor scrolls the WorkflowSettingsPanel into view on + * mount (U6/U9: redirect stubs link here via a `?panel=settings` param read by + * the editor's mount site). */ + initialPanel?: "settings"; } let nodeSeq = 0; @@ -122,6 +128,7 @@ function InnerEditor({ onClose, addToast, projectId, + initialPanel, modalRef, }: Omit & { modalRef: React.RefObject }) { const [workflows, setWorkflows] = useState([]); @@ -138,6 +145,13 @@ function InnerEditor({ const [columns, setColumns] = useState([]); // v2 custom field definitions the editor is authoring (KTD-13/14, U13). const [fields, setFields] = useState([]); + // v2 typed setting declarations the editor is authoring (U6, KTD-1). Setting + // VALUES live per-project in the workflow_settings table (KTD-2) and are + // managed by the panel's Values tab, not this declaration array. + const [settings, setSettings] = useState([]); + // Ref to the settings panel so a `?panel=settings` deep link can scroll it + // into view on mount (U6/U9 redirect stubs). + const settingsPanelRef = useRef(null); const [traitCatalog, setTraitCatalog] = useState([]); // Step-parser ids for the parse-steps inspector (KTD-12). Seeded with the // built-in pair so the select is never empty; replaced by the live catalog @@ -213,6 +227,7 @@ function InnerEditor({ setEdges([]); setColumns([]); setFields([]); + setSettings([]); return; } const flow = irToFlow(activeWorkflow); @@ -220,11 +235,25 @@ function InnerEditor({ setEdges(flow.edges); setColumns(columnsOf(activeWorkflow)); setFields(fieldsOf(activeWorkflow)); + setSettings(settingsOf(activeWorkflow)); setSelectedNodeId(null); setSelectedEdgeId(null); setValidationError(null); }, [activeWorkflow, setNodes, setEdges]); + // `?panel=settings` deep link (U6/U9 redirect stubs): once the active workflow + // has loaded, scroll the settings panel into view. Runs once per editor open. + const didScrollToSettings = useRef(false); + useEffect(() => { + if (initialPanel !== "settings" || didScrollToSettings.current) return; + if (!activeWorkflow) return; + const el = settingsPanelRef.current; + if (el) { + didScrollToSettings.current = true; + el.scrollIntoView({ behavior: "smooth", inline: "end", block: "nearest" }); + } + }, [initialPanel, activeWorkflow]); + // Server-reported node error (e.g. seam-in-branch) attributed to a node id. const [serverNodeError, setServerNodeError] = useState<{ nodeId: string; message: string } | null>(null); @@ -465,6 +494,7 @@ function InnerEditor({ edges, columns.length ? columns : undefined, fields.length ? fields : undefined, + settings.length ? settings : undefined, ); const updated = await updateWorkflow(activeWorkflow.id, { ir, layout }, projectId); setWorkflows((ws) => ws.map((w) => (w.id === updated.id ? updated : w))); @@ -491,7 +521,7 @@ function InnerEditor({ } finally { setSaving(false); } - }, [activeWorkflow, nodes, edges, columns, fields, unplaced, blockingViolationCount, projectId, addToast, t]); + }, [activeWorkflow, nodes, edges, columns, fields, settings, unplaced, blockingViolationCount, projectId, addToast, t]); // Stamp the shared error-state badge onto offending nodes: unplaced step // nodes and any node the server flagged (seam-in-branch). One component @@ -734,6 +764,19 @@ function InnerEditor({ /> )} + {activeWorkflow && ( +
+ +
+ )} + {selectedNode && selectedNode.data.kind !== "start" && selectedNode.data.kind !== "end" && (