diff --git a/.changeset/simplified-workflow-view.md b/.changeset/simplified-workflow-view.md new file mode 100644 index 0000000000..a8500ea820 --- /dev/null +++ b/.changeset/simplified-workflow-view.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add a simplified workflow editor view with a modern vertical canvas, plus Simple/Advanced/List mode toggle. +category: feature +dev: New WorkflowSimpleCanvas + WorkflowAddStepModal components; view mode persists in localStorage (`fusion:wf-editor-view-mode`, mobile `fusion:wf-mobile-graph-style`); insert-on-edge helpers live in workflow-simple-layout.ts. The old "Show simple editor" compact layout is now the List mode; the advanced canvas is unchanged. diff --git a/packages/dashboard/app/__tests__/css-hygiene-scan.test.ts b/packages/dashboard/app/__tests__/css-hygiene-scan.test.ts index cf178be91d..f0e927b452 100644 --- a/packages/dashboard/app/__tests__/css-hygiene-scan.test.ts +++ b/packages/dashboard/app/__tests__/css-hygiene-scan.test.ts @@ -130,7 +130,7 @@ describe("component CSS hygiene scan regressions", () => { ".plugin-registry-retry:focus-visible", ], "WorkflowNodeEditor.css": [ - ".wf-layout-toggle:focus-visible", + ".wf-view-mode-option:focus-visible", ".wf-template-option:focus-visible", ".wf-ai-prompt:focus-visible", ".wf-mobile-tab:focus-visible", diff --git a/packages/dashboard/app/components/WorkflowAddStepModal.css b/packages/dashboard/app/components/WorkflowAddStepModal.css new file mode 100644 index 0000000000..c0f37c8c59 --- /dev/null +++ b/packages/dashboard/app/components/WorkflowAddStepModal.css @@ -0,0 +1,228 @@ +/* +FNXC:WorkflowSimpleView 2026-07-10-12:00: +Add-step dialog for the simplified workflow view. Centered card on desktop, +full-width bottom sheet on mobile (mirrors the app's mobile modal pattern) so +the same dialog serves both breakpoints. +*/ + +.wf-add-step-overlay { + position: fixed; + inset: 0; + z-index: 1300; + display: flex; + align-items: center; + justify-content: center; + background: color-mix(in srgb, var(--bg) 62%, transparent); + backdrop-filter: blur(2px); +} + +.wf-add-step-dialog { + display: flex; + flex-direction: column; + width: min(680px, calc(100vw - 32px)); + max-height: min(72vh, 640px); + border-radius: var(--radius-xl); + border: 1px solid var(--border); + background: var(--surface); + box-shadow: var(--shadow-lg); + overflow: hidden; +} + +.wf-add-step-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px 10px; +} + +.wf-add-step-header h3 { + margin: 0; + font-size: 0.95rem; + color: var(--text); +} + +.wf-add-step-search { + display: flex; + align-items: center; + gap: 8px; + margin: 0 16px 12px; + padding: 8px 12px; + border-radius: var(--radius-md); + border: 1px solid var(--border); + background: var(--bg); + color: var(--text-muted); +} + +.wf-add-step-search:focus-within { + border-color: var(--todo); + box-shadow: var(--focus-ring); +} + +.wf-add-step-search-input { + flex: 1; + border: none; + outline: none; + background: transparent; + color: var(--text); + font-size: 0.85rem; +} + +.wf-add-step-conflict { + margin: 0 16px 10px; + padding: 8px 10px; + border-radius: var(--radius-md); + border: 1px solid color-mix(in srgb, var(--color-error) 45%, var(--border)); + background: var(--status-error-bg); + color: var(--color-error); + font-size: 0.75rem; +} + +.wf-add-step-body { + flex: 1; + overflow-y: auto; + padding: 0 16px 16px; + display: flex; + flex-direction: column; + gap: 14px; +} + +.wf-add-step-section h4 { + margin: 0 0 8px; + font-size: 0.68rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); +} + +.wf-add-step-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 8px; +} + +.wf-add-step-option { + display: flex; + align-items: flex-start; + gap: 10px; + width: 100%; + padding: 10px; + border-radius: var(--radius-lg); + border: 1px solid var(--border-subtle); + background: var(--surface-subtle); + color: var(--text); + text-align: left; + cursor: pointer; + transition: + background var(--transition-fast), + border-color var(--transition-fast); +} + +.wf-add-step-option:hover { + background: var(--surface-hover); + border-color: var(--border-strong); +} + +.wf-add-step-option:focus-visible { + outline: none; + border-color: var(--todo); + box-shadow: var(--focus-ring); +} + +.wf-add-step-option-chip { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + border-radius: var(--radius-md); + color: #fff; + background: var(--surface-emphasis); +} + +.wf-add-step-option--agent .wf-add-step-option-chip { + background: color-mix(in srgb, var(--ws-quality) 85%, var(--bg)); +} + +.wf-add-step-option--automation .wf-add-step-option-chip { + background: color-mix(in srgb, var(--ws-teal) 85%, var(--bg)); +} + +.wf-add-step-option--flow .wf-add-step-option-chip { + background: color-mix(in srgb, var(--color-merged) 85%, var(--bg)); +} + +.wf-add-step-option--template .wf-add-step-option-chip { + background: color-mix(in srgb, var(--triage) 80%, var(--bg)); +} + +.wf-add-step-option-text { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.wf-add-step-option-label { + font-size: 0.82rem; + font-weight: 600; +} + +.wf-add-step-option-desc { + font-size: 0.7rem; + color: var(--text-muted); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.wf-add-step-template-row { + display: flex; + flex-direction: column; + gap: 4px; +} + +.wf-add-step-optional { + align-self: flex-start; + display: inline-flex; + align-items: center; + gap: 5px; + margin-left: 40px; + padding: 3px 9px; + border-radius: var(--radius-pill); + border: 1px solid var(--border-subtle); + background: transparent; + color: var(--text-muted); + font-size: 0.68rem; + cursor: pointer; +} + +.wf-add-step-optional:hover { + border-color: var(--border-strong); + color: var(--text); +} + +.wf-add-step-empty { + margin: 6px 2px; + font-size: 0.8rem; + color: var(--text-muted); +} + +@media (max-width: 768px), (max-height: 480px) { + .wf-add-step-overlay { + align-items: flex-end; + } + + .wf-add-step-dialog { + width: 100vw; + max-height: 82dvh; + border-radius: var(--radius-xl) var(--radius-xl) 0 0; + border-bottom: none; + } + + .wf-add-step-grid { + grid-template-columns: 1fr; + } +} diff --git a/packages/dashboard/app/components/WorkflowAddStepModal.tsx b/packages/dashboard/app/components/WorkflowAddStepModal.tsx new file mode 100644 index 0000000000..0b00014d67 --- /dev/null +++ b/packages/dashboard/app/components/WorkflowAddStepModal.tsx @@ -0,0 +1,298 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { X, Search, Puzzle, ToggleRight } from "lucide-react"; +import type { WorkflowDefinition, WorkflowStepTemplate } from "@fusion/core"; +import type { WorkflowEditorNodeKind } from "./nodes/WorkflowNodeTypes"; +import { nodeHelpFor } from "./nodes/node-help"; +import "./WorkflowAddStepModal.css"; + +/* +FNXC:WorkflowSimpleView 2026-07-10-12:00: +The simplified workflow view's add-step surface. Requirements: + - One searchable dialog covering EVERYTHING the advanced palette + templates + toolbar offers (node kinds, fragments, Fusion step templates, plugin + templates) so the simple view loses no add capability. + - Node kinds are grouped into human categories (Agent steps / Automation / + Flow control) with one-line descriptions sourced from the node help + registry, because the flat 16-button advanced palette is the main thing + users found hard to use. + - When inserting INSIDE a container (foreach/loop/optional-group child + edge), container kinds are hidden — containers cannot nest. +*/ + +export interface AddStepPaletteEntry { + kind: WorkflowEditorNodeKind; + label: string; + icon: React.ComponentType<{ size?: number | string; "aria-hidden"?: boolean | "true" | "false" }>; + presetConfig?: Record; +} + +const CONTAINER_KINDS: ReadonlySet = new Set(["foreach", "loop", "optional-group"]); + +const AGENT_KINDS: ReadonlySet = new Set(["prompt", "ask-user", "gate", "step-review"]); +const AUTOMATION_KINDS: ReadonlySet = new Set(["script", "code", "notify", "parse-steps"]); + +export interface WorkflowAddStepModalProps { + open: boolean; + onClose: () => void; + palette: AddStepPaletteEntry[]; + /** Hide container kinds (insert target is inside a container). */ + disallowContainers?: boolean; + fragments: WorkflowDefinition[]; + stepTemplates: WorkflowStepTemplate[]; + pluginTemplates: Array<{ pluginId: string; template: WorkflowStepTemplate }>; + /** Persistent seam-duplication conflict notice (mirrors the toolbar's). */ + templateConflict?: string | null; + onPickPalette: (entry: AddStepPaletteEntry) => void; + onPickFragment: (fragment: WorkflowDefinition) => void; + onPickStepTemplate: (template: WorkflowStepTemplate) => void; + onPickStepTemplateAsOptionalGroup: (template: WorkflowStepTemplate) => void; +} + +export function WorkflowAddStepModal({ + open, + onClose, + palette, + disallowContainers = false, + fragments, + stepTemplates, + pluginTemplates, + templateConflict, + onPickPalette, + onPickFragment, + onPickStepTemplate, + onPickStepTemplateAsOptionalGroup, +}: WorkflowAddStepModalProps) { + const { t } = useTranslation("app"); + const [query, setQuery] = useState(""); + const searchRef = useRef(null); + + useEffect(() => { + if (open) { + setQuery(""); + // Focus after the dialog paints. + const id = window.setTimeout(() => searchRef.current?.focus(), 0); + return () => window.clearTimeout(id); + } + }, [open]); + + const q = query.trim().toLowerCase(); + + const categories = useMemo(() => { + const eligible = palette.filter((entry) => !(disallowContainers && CONTAINER_KINDS.has(entry.kind))); + const withHelp = eligible.map((entry) => ({ + entry, + description: nodeHelpFor(entry.kind)?.summary ?? "", + })); + const matches = (item: { entry: AddStepPaletteEntry; description: string }) => + !q || item.entry.label.toLowerCase().includes(q) || item.entry.kind.includes(q) || item.description.toLowerCase().includes(q); + return [ + { + id: "agent", + label: t("workflowNodes.addStepAgentSteps", "Agent steps"), + items: withHelp.filter((item) => AGENT_KINDS.has(item.entry.kind)).filter(matches), + }, + { + id: "automation", + label: t("workflowNodes.addStepAutomation", "Automation"), + items: withHelp.filter((item) => AUTOMATION_KINDS.has(item.entry.kind)).filter(matches), + }, + { + id: "flow", + label: t("workflowNodes.addStepFlowControl", "Flow control"), + items: withHelp + .filter((item) => !AGENT_KINDS.has(item.entry.kind) && !AUTOMATION_KINDS.has(item.entry.kind)) + .filter(matches), + }, + ].filter((category) => category.items.length > 0); + }, [palette, disallowContainers, q, t]); + + const filteredFragments = useMemo( + () => fragments.filter((f) => !q || f.name.toLowerCase().includes(q)), + [fragments, q], + ); + const filteredStepTemplates = useMemo( + () => stepTemplates.filter((s) => !q || s.name.toLowerCase().includes(q)), + [stepTemplates, q], + ); + const filteredPluginTemplates = useMemo( + () => pluginTemplates.filter((p) => !q || p.template.name.toLowerCase().includes(q)), + [pluginTemplates, q], + ); + const hasTemplates = + filteredFragments.length > 0 || filteredStepTemplates.length > 0 || filteredPluginTemplates.length > 0; + const hasAnyResult = categories.length > 0 || hasTemplates; + + if (!open) return null; + + return ( +
{ + if (e.key === "Escape") { + e.stopPropagation(); + onClose(); + } + }} + > +
e.stopPropagation()} + > +
+

{t("workflowNodes.addStepTitle", "Add a step")}

+ +
+
+ + setQuery(e.target.value)} + /> +
+ {templateConflict && ( +
+ {t( + "workflowNodes.templateSeamConflict", + 'This fragment duplicates the "{{seam}}" seam already on the canvas, so it can\'t be inserted.', + { seam: templateConflict }, + )} +
+ )} +
+ {categories.map((category) => ( +
+

{category.label}

+
+ {category.items.map(({ entry, description }) => { + const Icon = entry.icon; + return ( + + ); + })} +
+
+ ))} + {hasTemplates && ( +
+

{t("workflowNodes.templatesSection", "Templates")}

+
+ {filteredFragments.map((fragment) => ( + + ))} + {filteredStepTemplates.map((template) => ( +
+ + {!disallowContainers && ( + + )} +
+ ))} + {filteredPluginTemplates.map(({ pluginId, template }) => ( + + ))} +
+
+ )} + {!hasAnyResult && ( +

+ {t("workflowNodes.addStepNoMatches", "No steps match “{{query}}”.", { query })} +

+ )} +
+
+
+ ); +} diff --git a/packages/dashboard/app/components/WorkflowNodeEditor.css b/packages/dashboard/app/components/WorkflowNodeEditor.css index c0f64fd5d5..2d7db49766 100644 --- a/packages/dashboard/app/components/WorkflowNodeEditor.css +++ b/packages/dashboard/app/components/WorkflowNodeEditor.css @@ -797,30 +797,110 @@ Flow's SVG attributes and makes the graph preview read blank. display: none; } -.wf-layout-toggle { +/* +FNXC:WorkflowSimpleView 2026-07-10-12:00: +Segmented three-mode view switch (Simple / Advanced / List) replacing the old +boolean .wf-layout-toggle. Also reused by the mobile graph-style toggle +(Graph / List) so both switches read as one control family. +*/ +.wf-view-mode-toggle, +.wf-mobile-graph-style-toggle { + display: inline-flex; + align-items: center; + width: fit-content; + padding: 2px; + border: 1px solid var(--border); + border-radius: var(--radius-pill); + background: var(--bg-secondary); +} + +.wf-mobile-graph-style-toggle { + margin: var(--space-sm) var(--space-md) 0; +} + +.wf-view-mode-option { display: inline-flex; align-items: center; gap: var(--space-xs); - width: fit-content; - min-height: 30px; - padding: var(--space-xs) var(--space-sm); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - background: var(--bg-secondary); + min-height: 26px; + padding: var(--space-xs) 10px; + border: none; + border-radius: var(--radius-pill); + background: transparent; color: var(--text-muted); font: inherit; - font-size: 0.8rem; + font-size: 0.78rem; cursor: pointer; + transition: + background var(--transition-fast), + color var(--transition-fast); } -.wf-layout-toggle:hover { - background: var(--bg-tertiary); +.wf-view-mode-option:hover { color: var(--text); } -.wf-layout-toggle:focus-visible { +.wf-view-mode-option--active { + background: var(--surface-emphasis); + color: var(--text); + box-shadow: var(--shadow-sm); +} + +.wf-view-mode-option:focus-visible { outline: 2px solid var(--accent); - outline-offset: 2px; + outline-offset: 1px; +} + +/* Simplified-view toolbar: a single quiet row (Add step + common actions). */ +.wf-editor-toolbar--simple { + justify-content: space-between; +} + +.wf-editor-toolbar--simple .wf-simple-toolbar-add { + border-color: color-mix(in srgb, var(--todo) 45%, var(--border)); + color: var(--text); +} + +.wf-editor-toolbar--simple .wf-simple-toolbar-add:hover { + border-color: var(--todo); + background: color-mix(in srgb, var(--todo) 12%, var(--surface)); +} + +/* Mobile graph tab: the simplified canvas needs a bounded, non-scrolling + region inside the scrollable mobile panel (WorkflowSimpleCanvas fills it + with position:absolute). */ +.wf-mobile-simple-canvas { + position: relative; + flex: 1 1 auto; + min-height: max(340px, 55dvh); + margin: var(--space-sm) var(--space-md) var(--space-md); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + overflow: hidden; +} + +/* +FNXC:WorkflowSimpleView 2026-07-10-12:00: +Simple-view inspector polish: the shared node/edge inspector gets a roomier, +card-like presentation in the simplified view (wider pane, soft section +cards) without touching its advanced-view layout or any field markup. +*/ +.wf-editor-inspector--simple { + width: 320px; + padding: var(--space-lg); + gap: var(--space-md); + background: var(--surface); +} + +.wf-editor-inspector--simple h3 { + font-size: 1rem; +} + +.wf-editor-inspector--simple .wf-inspector-fields { + padding: var(--space-md); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + background: var(--surface-subtle); } .wf-editor-body--simple-layout :where(.wf-editor-readonly-banner), diff --git a/packages/dashboard/app/components/WorkflowNodeEditor.tsx b/packages/dashboard/app/components/WorkflowNodeEditor.tsx index 74e64f8287..b74b7beb52 100644 --- a/packages/dashboard/app/components/WorkflowNodeEditor.tsx +++ b/packages/dashboard/app/components/WorkflowNodeEditor.tsx @@ -91,6 +91,9 @@ import { FOREACH_CHILD_Y, } from "./workflow-flow-mapping"; import { autoLayout, applyAutoLayout } from "./workflow-auto-layout"; +import { insertNodeOnEdge, findAppendEdgeId } from "./workflow-simple-layout"; +import { WorkflowSimpleCanvas } from "./WorkflowSimpleCanvas"; +import { WorkflowAddStepModal, type AddStepPaletteEntry } from "./WorkflowAddStepModal"; import { fetchTraits, fetchStepParsers, type TraitCatalogEntry } from "../api"; import { WorkflowColumnPanel } from "./WorkflowColumnPanel"; import { WorkflowFieldsPanel } from "./WorkflowFieldsPanel"; @@ -107,6 +110,27 @@ import { } from "./workflow-mobile-graph"; type ExecutorKind = "model" | "agent" | "skill" | "cli" | "cli-agent"; + +/* +FNXC:WorkflowSimpleView 2026-07-10-12:00: +The desktop workflow editor now has THREE view modes over the same node/edge +state, replacing the old boolean simple-editor toggle: + - "simple" — the simplified graphical node editor (default): vertical + auto-laid-out canvas, "+" insert-on-edge, add-step dialog, no palette + toolbar or column bands. Cleaner for the common tasks (adding and + configuring nodes). + - "advanced" — the full canvas: palette toolbar, templates, drag/connect, + column swimlanes, minimap, auto-layout, import/export. Nothing removed. + - "list" — the pre-existing row-list simple editor, kept as an even + simpler fallback; it reuses the mobile shell exactly as before. +The choice persists per-browser in localStorage. Mobile keeps its tab shell +(forced by viewport) and gets its own graph-style toggle: the simplified +canvas by default with the row list as fallback. +*/ +type WorkflowEditorViewMode = "simple" | "advanced" | "list"; +const viewModeStorageKey = "fusion:wf-editor-view-mode"; +type MobileGraphStyle = "canvas" | "list"; +const mobileGraphStyleStorageKey = "fusion:wf-mobile-graph-style"; // FNXC:WorkflowOptionalGroup 2026-06-21-18:00: dropped the "optional-steps" mobile // panel — the declaration authoring surface is retired (optional-group nodes now). type MobileWorkflowPanel = "graph" | "add" | "settings" | "fields" | "columns" | "actions"; @@ -802,11 +826,49 @@ function InnerEditor({ const [selectedEdgeId, setSelectedEdgeId] = useState(null); const [inspectorCollapsed, setInspectorCollapsed] = useState(false); const [miniMapCollapsed, setMiniMapCollapsed] = useState(false); - const [compactLayoutEnabled, setCompactLayoutEnabled] = useState(false); + // FNXC:WorkflowSimpleView 2026-07-10-12:00: three-mode desktop view state + // (see the WorkflowEditorViewMode note above). Persisted so operators land + // back in the mode they work in; "simple" is the default for everyone else. + const [viewMode, setViewMode] = useState(() => { + try { + const stored = localStorage.getItem(viewModeStorageKey); + if (stored === "simple" || stored === "advanced" || stored === "list") return stored; + } catch { + // localStorage unavailable: non-fatal. + } + return "simple"; + }); + useEffect(() => { + try { + localStorage.setItem(viewModeStorageKey, viewMode); + } catch { + // localStorage unavailable (private mode / SSR): non-fatal. + } + }, [viewMode]); + // Mobile graph tab presentation: simplified canvas (default) or row list. + const [mobileGraphStyle, setMobileGraphStyle] = useState(() => { + try { + const stored = localStorage.getItem(mobileGraphStyleStorageKey); + if (stored === "canvas" || stored === "list") return stored; + } catch { + // localStorage unavailable: non-fatal. + } + return "canvas"; + }); + useEffect(() => { + try { + localStorage.setItem(mobileGraphStyleStorageKey, mobileGraphStyle); + } catch { + // localStorage unavailable (private mode / SSR): non-fatal. + } + }, [mobileGraphStyle]); const [mobilePanel, setMobilePanel] = useState(() => initialPanel === "settings" ? "settings" : "graph", ); - const simpleLayoutEnabled = isMobileMode || compactLayoutEnabled; + // "list" keeps the pre-existing compact (mobile-shell) presentation. + const simpleLayoutEnabled = isMobileMode || viewMode === "list"; + // Desktop simplified graphical view (not the list fallback, not mobile). + const simpleViewEnabled = !simpleLayoutEnabled && viewMode === "simple"; const { t } = useTranslation("app"); const { confirm } = useConfirm(); // Create-workflow dialog (KTD-7) open state + focus-return ref to the @@ -1606,6 +1668,85 @@ function InnerEditor({ [isBuiltin, nodes, edges, activeWorkflow, setNodes, setEdges], ); + /* + FNXC:WorkflowSimpleView 2026-07-10-12:00: + Add-step dialog target for the simplified view. `edgeId` set = insert ON that + edge (source→new→target rewiring via insertNodeOnEdge); null = free append + (the "+ Add step" pill), which prefers the unambiguous edge into `end` and + falls back to the classic free-floating addNode. `insideContainer` hides + container kinds from the dialog because containers cannot nest. + */ + const [addStepTarget, setAddStepTarget] = useState<{ + edgeId: string | null; + insideContainer: boolean; + } | null>(null); + + const openInsertOnEdge = useCallback( + (edgeId: string) => { + if (isBuiltin) return; + const edge = edges.find((e) => e.id === edgeId); + const source = edge ? nodes.find((n) => n.id === edge.source) : undefined; + const target = edge ? nodes.find((n) => n.id === edge.target) : undefined; + const insideContainer = !!source?.parentId && source.parentId === target?.parentId; + setAddStepTarget({ edgeId, insideContainer }); + }, + [isBuiltin, edges, nodes], + ); + + const openAddStep = useCallback(() => { + if (isBuiltin) return; + setAddStepTarget({ edgeId: findAppendEdgeId(nodes, edges), insideContainer: false }); + }, [isBuiltin, nodes, edges]); + + const containerChildLabelFor = useCallback( + (kind: WorkflowEditorNodeKind) => + kind === "foreach" + ? t("workflowNodes.stepExecuteLabel", "Step execute") + : kind === "optional-group" + ? t("workflowNodes.optionalGroupStepLabel", "Optional step") + : t("workflowNodes.loopStepLabel", "Loop step"), + [t], + ); + + /** Insert on the targeted edge when one is set (falling back to addNode when + * the edge disappeared, e.g. deleted while the dialog was open). */ + const insertFromAddStep = useCallback( + (kind: WorkflowEditorNodeKind, label: string, presetConfig?: Record) => { + if (addStepTarget?.edgeId) { + const result = insertNodeOnEdge(nodes, edges, addStepTarget.edgeId, { + kind, + label, + presetConfig, + containerChildLabel: containerChildLabelFor(kind), + }); + if (result) { + setNodes(result.nodes); + setEdges(result.edges); + setSelectedNodeId(result.newNodeId); + setSelectedEdgeId(null); + setAddStepTarget(null); + return; + } + } + addNode(kind, label, presetConfig); + setAddStepTarget(null); + }, + [addStepTarget, nodes, edges, setNodes, setEdges, addNode, containerChildLabelFor], + ); + + const handleAddStepPalettePick = useCallback( + (entry: AddStepPaletteEntry) => insertFromAddStep(entry.kind, entry.label, entry.presetConfig), + [insertFromAddStep], + ); + + const handleAddStepTemplatePick = useCallback( + (tpl: WorkflowStepTemplate) => { + const { kind, label, config } = stepTemplateToNode(tpl); + insertFromAddStep(kind, label, config); + }, + [insertFromAddStep], + ); + // Auto-layout: one-click left-to-right tidy (U5, R8). Recomputes positions // only; bands and foreach template children are left in place. Marks the // editor dirty automatically via the layout serialization in isDirty. @@ -2321,6 +2462,8 @@ function InnerEditor({ }), [models, agents, skills], ); + // Column id → display name for the simplified view's per-node column chips. + const columnNameMap = useMemo(() => new Map(columns.map((c) => [c.id, c.name])), [columns]); const mobileConnectionTargetsBySource = useMemo(() => { const targetNodes = nodesForRender .filter((node) => !isColumnBandNode(node.id) && node.data.kind !== "start") @@ -2604,7 +2747,7 @@ function InnerEditor({
@@ -2700,7 +2843,10 @@ function InnerEditor({ workflow is active (read-only gating preserved via isBuiltin). The disclosure button serves as the section header; the panels' own internal

is suppressed via CSS to avoid a double header. */} - {activeWorkflow && !simpleLayoutEnabled && ( + {/* FNXC:WorkflowSimpleView 2026-07-10-12:00: columns/fields/settings + authoring panels are advanced-view chrome; the simplified view + hides them (still one click away via the Advanced toggle). */} + {activeWorkflow && !simpleLayoutEnabled && !simpleViewEnabled && (
+ {( + [ + ["simple", Workflow, t("workflows.viewModeSimple", "Simple")], + ["advanced", LayoutGrid, t("workflows.viewModeAdvanced", "Advanced")], + ["list", ListChecks, t("workflows.viewModeList", "List")], + ] as Array<[WorkflowEditorViewMode, typeof Workflow, string]> + ).map(([mode, Icon, label]) => ( + + ))} +
)}

{lifecycleWarnings.length > 0 && ( @@ -2958,23 +3119,89 @@ function InnerEditor({
{mobilePanel === "graph" && ( - { - setSelectedNodeId(id); - setSelectedEdgeId(null); - setInspectorCollapsed(false); - }} - onSelectEdge={(id) => { - setSelectedEdgeId(id); - setSelectedNodeId(null); - }} - onCreateConnection={isBuiltin ? undefined : onCreateSimpleConnection} - canReorder={!isBuiltin} - onMoveNode={onMoveSimpleNode} - /> + <> + {/* FNXC:WorkflowSimpleView 2026-07-10-12:00: mobile + graph tab offers the simplified touch canvas + (default) with the row list kept as the even + simpler fallback. Desktop "list" mode reuses + this shell and keeps the row list only. */} + {isMobileMode && ( +
+ {( + [ + ["canvas", t("workflowNodes.mobileGraphStyleCanvas", "Graph")], + ["list", t("workflowNodes.mobileGraphStyleList", "List")], + ] as Array<[MobileGraphStyle, string]> + ).map(([style, label]) => ( + + ))} +
+ )} + {isMobileMode && mobileGraphStyle === "canvas" ? ( +
+ + { + setSelectedNodeId(id); + setSelectedEdgeId(null); + setInspectorCollapsed(false); + }} + onSelectEdge={(id) => { + setSelectedEdgeId(id); + setSelectedNodeId(null); + }} + onClearSelection={() => { + setSelectedNodeId(null); + setSelectedEdgeId(null); + }} + onInsertOnEdge={openInsertOnEdge} + onAddStep={openAddStep} + onBeforeDelete={onBeforeDelete} + onNodesDelete={onNodesDelete} + onEdgesDelete={onEdgesDelete} + /> + +
+ ) : ( + { + setSelectedNodeId(id); + setSelectedEdgeId(null); + setInspectorCollapsed(false); + }} + onSelectEdge={(id) => { + setSelectedEdgeId(id); + setSelectedNodeId(null); + }} + onCreateConnection={isBuiltin ? undefined : onCreateSimpleConnection} + canReorder={!isBuiltin} + onMoveNode={onMoveSimpleNode} + /> + )} + )} {mobilePanel === "add" && ( @@ -3256,6 +3483,96 @@ function InnerEditor({ {t("workflows.duplicateToCustomize", "Duplicate to customize")}
+ ) : simpleViewEnabled ? ( + /* FNXC:WorkflowSimpleView 2026-07-10-12:00: simplified-view + toolbar — the flat 16-button palette moves into the + searchable add-step dialog; the toolbar keeps only the + common actions (Add step, Design with AI, Delete, Save). + Import/Export/Auto-layout stay in the Advanced view. */ +
+ +
+
+ + {aiPanelOpen && ( +
+