@agentforge4j/workflow-builder-react 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3538 @@
1
+ // src/api/WorkflowBuilder.tsx
2
+ import { useCallback as useCallback5, useEffect as useEffect3, useMemo as useMemo4, useRef, useState as useState6 } from "react";
3
+
4
+ // src/canvas/WorkflowCanvas.tsx
5
+ import "@xyflow/react/dist/style.css";
6
+
7
+ // src/copy/workflow-terminology.ts
8
+ var NODE_LABELS = {
9
+ ASK_USER: "Ask a Question",
10
+ AI_STEP: "AI Step",
11
+ AI_DEBATE: "AI with Tools",
12
+ DECISION: "Decision",
13
+ REPEAT: "Loop / Repeat",
14
+ REUSE_WORKFLOW: "Reuse Existing Workflow",
15
+ LOAD_RESOURCE: "Load External Resource",
16
+ STOP: "Stop with Error",
17
+ RETRY: "Retry Previous Step",
18
+ SAVE_RESULT: "Save Result"
19
+ };
20
+ var NODE_DESCRIPTIONS = {
21
+ ASK_USER: "Collect input from the person running the workflow.",
22
+ AI_STEP: "Run an AI prompt and capture its output.",
23
+ AI_DEBATE: "Run an AI step that can call tools and external resources.",
24
+ DECISION: "Choose a path based on a condition.",
25
+ REPEAT: "Repeat a section of the workflow.",
26
+ REUSE_WORKFLOW: "Run another workflow as a step.",
27
+ LOAD_RESOURCE: "Reference an external resource (data, API, document).",
28
+ STOP: "Stop the workflow with an error message.",
29
+ RETRY: "Retry an earlier step.",
30
+ SAVE_RESULT: "Save the workflow's result."
31
+ };
32
+ var ACTION_LABELS = {
33
+ validate: "Check Workflow",
34
+ validating: "Checking...",
35
+ clientValidation: "Workflow Problems",
36
+ serverValidation: "Server Problems",
37
+ reuseWorkflow: "Reuse Existing Workflow",
38
+ saveDraft: "Save draft",
39
+ save: "Save",
40
+ saving: "Saving\u2026",
41
+ run: "Run",
42
+ running: "Running\u2026",
43
+ publish: "Publish",
44
+ publishing: "Publishing\u2026",
45
+ import: "Import",
46
+ importing: "Importing\u2026",
47
+ export: "Export",
48
+ exporting: "Exporting\u2026",
49
+ install: "Install",
50
+ downloadBundle: "Download bundle",
51
+ looksGood: "Looks good",
52
+ thingsToFix: (count) => count === 1 ? "1 thing to fix" : `${count} things to fix`,
53
+ unsavedChanges: "Unsaved changes",
54
+ upToDate: "Up to date",
55
+ validationIssues: "Validation issues",
56
+ builderTitle: "Workflow Builder",
57
+ aiAssist: "Build with AI",
58
+ guidedMode: "Guided",
59
+ advancedMode: "Advanced",
60
+ deleteNode: "Delete node",
61
+ selectStartStep: "Select start step",
62
+ fixIssue: "Fix",
63
+ workflowLooksGood: "Workflow looks good",
64
+ configureStepClose: "Configure this step and close when done.",
65
+ nameField: "Name",
66
+ typeField: "Type",
67
+ descriptionField: "Description",
68
+ questionField: "Question",
69
+ agentField: "Agent",
70
+ agentPlaceholder: "Agent id",
71
+ instructionsField: "Instructions",
72
+ approvalField: "Approval",
73
+ approvalAutomatic: "Automatic",
74
+ approvalReview: "Requires human review",
75
+ approvalRequired: "Requires human approval",
76
+ approvalHelp: "When set to 'Requires human approval', the workflow pauses here for a person to review before continuing.",
77
+ basicsSection: "Basics",
78
+ inputsOutputsSection: "Inputs / Outputs",
79
+ behaviorSection: "Behavior",
80
+ advancedSection: "Advanced",
81
+ addField: "Add field",
82
+ resultNameField: "Result name",
83
+ resourcePathField: "Resource path",
84
+ resultContextKeyField: "Result name (context key)",
85
+ reasonField: "Reason",
86
+ targetStepField: "Target step",
87
+ fallbackTargetField: "Fallback target",
88
+ maxRetriesField: "Max retries",
89
+ maxAttemptsField: "Max attempts",
90
+ primaryAgentField: "Primary agent",
91
+ challengerAgentField: "Challenger agent",
92
+ maxRoundsField: "Max rounds",
93
+ resolutionInstructionsField: "Resolution instructions",
94
+ contextKeyField: "Context key",
95
+ contextKeyHint: "Use the key of a value the previous step stored in context.",
96
+ caseLabelPlaceholder: "Case label",
97
+ caseValuePlaceholder: "Case value",
98
+ connectsToField: "Connects to",
99
+ addCase: "Add case",
100
+ defaultBranchTargetField: "Default branch target",
101
+ loopModeField: "Mode",
102
+ maxIterationsField: "Max iterations",
103
+ listContextKeyField: "List context key",
104
+ evaluatorAgentField: "Evaluator agent",
105
+ maxIterationsActionField: "When max iterations reached",
106
+ loopBodyReadOnly: "This step is inside a loop body. Nested editing is not ready yet, so its fields are read-only.",
107
+ advancedStepsGuided: "Advanced steps (for power users)",
108
+ advancedSteps: "Advanced steps",
109
+ addStep: "Add step",
110
+ chooseStepDescription: "Choose a step to add to your workflow.",
111
+ selectPlaceholder: "Select\u2026",
112
+ unsupportedBannerTitle: "Some advanced workflow parts are shown read-only because editing support is not ready yet.",
113
+ builderTipsTitle: "Builder tips",
114
+ builderTipSelect: "Select a step to configure it in the side panel.",
115
+ builderTipConnect: "Drag from a connection port to link steps \u2014 arrows show execution order.",
116
+ builderTipLibrary: "Open the step library on the left to add more steps.",
117
+ dismissTips: "Dismiss builder tips",
118
+ otherwiseBranch: "Otherwise",
119
+ loopBodyTitle: "Loop body",
120
+ loopBodyEmpty: "Connect steps inside this loop.",
121
+ workflowIdLabel: "Workflow id",
122
+ workflowNameLabel: "Workflow name",
123
+ workflowNamePlaceholder: "Workflow name",
124
+ workflowIdPlaceholder: "workflow-id",
125
+ modePickerTitle: "How would you like to build?",
126
+ modePickerGuidedHint: "A simple checklist places each step on the canvas for you.",
127
+ modePickerAdvancedHint: "Full step library and every setting from the start.",
128
+ validationDetailsDescription: "Review problems and jump to the step that needs a fix.",
129
+ formFieldsHint: "Form fields are listed below; each needs a label and key.",
130
+ fixedCountMode: "Fixed count",
131
+ forEachMode: "For each item",
132
+ agentSignalMode: "Until AI says done",
133
+ evaluatorMode: "Evaluator says done",
134
+ awaitUserAction: "Ask user",
135
+ failAction: "Stop with error",
136
+ okShort: "OK",
137
+ checkmarkOk: "\u2713 OK"
138
+ };
139
+ var BUILDER_COPY = {
140
+ startHere: "Start here \u2014 this step collects input from whoever runs the workflow.",
141
+ useTemplate: "Use a template",
142
+ templateHint: "Browse starter templates on the home page.",
143
+ modePickerTitle: ACTION_LABELS.modePickerTitle,
144
+ modePickerGuided: ACTION_LABELS.guidedMode,
145
+ modePickerGuidedHint: ACTION_LABELS.modePickerGuidedHint,
146
+ modePickerAdvanced: ACTION_LABELS.advancedMode,
147
+ modePickerAdvancedHint: ACTION_LABELS.modePickerAdvancedHint,
148
+ paletteTitle: "Steps",
149
+ expandPalette: "Expand step library",
150
+ collapsePalette: "Collapse step library",
151
+ validationDetails: "Validation details",
152
+ stepLibrary: "Step library"
153
+ };
154
+ var EDGE_LABELS = {
155
+ approvalGate: "Approval",
156
+ reviewGate: "Review"
157
+ };
158
+ var NODE_STATUS_LABELS = {
159
+ valid: "Ready",
160
+ hasIssues: "Needs attention",
161
+ needsApproval: "Approval required"
162
+ };
163
+ var ISSUE_REWRITES = {
164
+ "workflow id is required": "Add a workflow ID before validating.",
165
+ "workflow name is required": "Give your workflow a name.",
166
+ "at least one step is required": "Add at least one step to the workflow.",
167
+ "step name is required": "Name this step so it is easier to understand.",
168
+ "must be >= 1": "Use a value of 1 or more."
169
+ };
170
+ var GUIDED_STAGE_LABELS = {
171
+ addInput: "Add input",
172
+ configureInput: "Configure input",
173
+ addAiStep: "Add AI step",
174
+ addApproval: "Add approval",
175
+ requireApproval: "Require approval",
176
+ generateResult: "Generate result",
177
+ addSaveStep: "Add save step"
178
+ };
179
+
180
+ // src/canvas/edges/FlowEdge.tsx
181
+ import { BaseEdge, EdgeLabelRenderer, getBezierPath } from "@xyflow/react";
182
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
183
+ function edgeStroke(transition) {
184
+ if (transition === "HUMAN_APPROVAL") {
185
+ return "var(--builder-color-edge-approval)";
186
+ }
187
+ if (transition === "HUMAN_REVIEW") {
188
+ return "var(--builder-color-edge-review)";
189
+ }
190
+ return "var(--builder-color-edge)";
191
+ }
192
+ function edgeDash(transition) {
193
+ if (transition === "HUMAN_REVIEW") {
194
+ return "6 4";
195
+ }
196
+ return void 0;
197
+ }
198
+ function FlowEdge({
199
+ id,
200
+ sourceX,
201
+ sourceY,
202
+ targetX,
203
+ targetY,
204
+ sourcePosition,
205
+ targetPosition,
206
+ selected,
207
+ markerEnd,
208
+ data
209
+ }) {
210
+ const transition = data?.transition ?? null;
211
+ const [path, labelX, labelY] = getBezierPath({
212
+ sourceX,
213
+ sourceY,
214
+ sourcePosition,
215
+ targetX,
216
+ targetY,
217
+ targetPosition
218
+ });
219
+ const stroke = edgeStroke(transition);
220
+ const label = transition === "HUMAN_APPROVAL" ? EDGE_LABELS.approvalGate : transition === "HUMAN_REVIEW" ? EDGE_LABELS.reviewGate : null;
221
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
222
+ /* @__PURE__ */ jsx(
223
+ BaseEdge,
224
+ {
225
+ id,
226
+ path,
227
+ style: {
228
+ strokeWidth: selected ? 2.75 : 2,
229
+ stroke,
230
+ strokeDasharray: edgeDash(transition)
231
+ },
232
+ markerEnd
233
+ }
234
+ ),
235
+ label ? /* @__PURE__ */ jsx(EdgeLabelRenderer, { children: /* @__PURE__ */ jsx(
236
+ "div",
237
+ {
238
+ className: "wf-edge-label nodrag nopan",
239
+ style: {
240
+ transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`
241
+ },
242
+ children: label
243
+ }
244
+ ) }) : null
245
+ ] });
246
+ }
247
+
248
+ // src/canvas/empty/EmptyState.tsx
249
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
250
+ function EmptyState({
251
+ icon,
252
+ title,
253
+ description,
254
+ action,
255
+ primaryAction,
256
+ secondaryAction,
257
+ className
258
+ }) {
259
+ const resolvedAction = action ?? (primaryAction || secondaryAction ? /* @__PURE__ */ jsxs2("div", { className: "wf-empty-state__actions", children: [
260
+ primaryAction ? /* @__PURE__ */ jsx2("button", { type: "button", className: "wf-button wf-button--primary", onClick: primaryAction.onClick, children: primaryAction.label }) : null,
261
+ secondaryAction ? /* @__PURE__ */ jsx2("button", { type: "button", className: "wf-button wf-button--secondary", onClick: secondaryAction.onClick, children: secondaryAction.label }) : null
262
+ ] }) : null);
263
+ return /* @__PURE__ */ jsxs2("div", { className: ["wf-empty-state", className].filter(Boolean).join(" "), children: [
264
+ icon ?? null,
265
+ /* @__PURE__ */ jsx2("p", { className: "wf-empty-state__title", children: title }),
266
+ description ? /* @__PURE__ */ jsx2("p", { className: "wf-empty-state__description", children: description }) : null,
267
+ resolvedAction
268
+ ] });
269
+ }
270
+
271
+ // src/model/nodeKinds.ts
272
+ var NODE_KIND_META = {
273
+ ASK_USER: {
274
+ label: NODE_LABELS.ASK_USER,
275
+ description: NODE_DESCRIPTIONS.ASK_USER,
276
+ iconGlyph: "?"
277
+ },
278
+ AI_STEP: {
279
+ label: NODE_LABELS.AI_STEP,
280
+ description: NODE_DESCRIPTIONS.AI_STEP,
281
+ iconGlyph: "AI"
282
+ },
283
+ AI_DEBATE: {
284
+ label: NODE_LABELS.AI_DEBATE,
285
+ description: NODE_DESCRIPTIONS.AI_DEBATE,
286
+ iconGlyph: "DB",
287
+ advanced: true
288
+ },
289
+ DECISION: {
290
+ label: NODE_LABELS.DECISION,
291
+ description: NODE_DESCRIPTIONS.DECISION,
292
+ iconGlyph: "IF"
293
+ },
294
+ REPEAT: {
295
+ label: NODE_LABELS.REPEAT,
296
+ description: NODE_DESCRIPTIONS.REPEAT,
297
+ iconGlyph: "\u21BB"
298
+ },
299
+ REUSE_WORKFLOW: {
300
+ label: NODE_LABELS.REUSE_WORKFLOW,
301
+ description: NODE_DESCRIPTIONS.REUSE_WORKFLOW,
302
+ iconGlyph: "WF"
303
+ },
304
+ LOAD_RESOURCE: {
305
+ label: NODE_LABELS.LOAD_RESOURCE,
306
+ description: NODE_DESCRIPTIONS.LOAD_RESOURCE,
307
+ iconGlyph: "LD",
308
+ advanced: true
309
+ },
310
+ STOP: {
311
+ label: NODE_LABELS.STOP,
312
+ description: NODE_DESCRIPTIONS.STOP,
313
+ iconGlyph: "!",
314
+ advanced: true
315
+ },
316
+ RETRY: {
317
+ label: NODE_LABELS.RETRY,
318
+ description: NODE_DESCRIPTIONS.RETRY,
319
+ iconGlyph: "RT",
320
+ advanced: true
321
+ },
322
+ SAVE_RESULT: {
323
+ label: NODE_LABELS.SAVE_RESULT,
324
+ description: NODE_DESCRIPTIONS.SAVE_RESULT,
325
+ iconGlyph: "SV"
326
+ }
327
+ };
328
+ var LIBRARY_COMMON_KINDS = [
329
+ "ASK_USER",
330
+ "AI_STEP",
331
+ "DECISION",
332
+ "REPEAT",
333
+ "SAVE_RESULT",
334
+ "REUSE_WORKFLOW"
335
+ ];
336
+ var LIBRARY_ADVANCED_KINDS = ["AI_DEBATE", "LOAD_RESOURCE", "STOP", "RETRY"];
337
+
338
+ // src/canvas/nodes/NodeChrome.tsx
339
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
340
+ function StatusBadge({ issueCount, needsApproval }) {
341
+ if (issueCount > 0) {
342
+ return /* @__PURE__ */ jsxs3("span", { className: "wf-node-badge wf-node-badge--error", children: [
343
+ /* @__PURE__ */ jsx3("span", { className: "wf-node-badge__glyph", "aria-hidden": true, children: "!" }),
344
+ NODE_STATUS_LABELS.hasIssues
345
+ ] });
346
+ }
347
+ if (needsApproval) {
348
+ return /* @__PURE__ */ jsxs3("span", { className: "wf-node-badge wf-node-badge--warning", children: [
349
+ /* @__PURE__ */ jsx3("span", { className: "wf-node-badge__glyph", "aria-hidden": true, children: "A" }),
350
+ NODE_STATUS_LABELS.needsApproval
351
+ ] });
352
+ }
353
+ return /* @__PURE__ */ jsxs3("span", { className: "wf-node-badge wf-node-badge--success", children: [
354
+ /* @__PURE__ */ jsx3("span", { className: "wf-node-badge__glyph", "aria-hidden": true, children: "\u2713" }),
355
+ NODE_STATUS_LABELS.valid
356
+ ] });
357
+ }
358
+ function NodeChrome({
359
+ kind,
360
+ variant = "step",
361
+ title,
362
+ subtitle: subtitle2,
363
+ selected,
364
+ issueCount = 0,
365
+ needsApproval = false,
366
+ children,
367
+ className
368
+ }) {
369
+ const meta = NODE_KIND_META[kind];
370
+ return /* @__PURE__ */ jsxs3(
371
+ "div",
372
+ {
373
+ className: [
374
+ "wf-node",
375
+ `wf-node--${variant}`,
376
+ selected ? "wf-node--selected" : "",
377
+ className ?? ""
378
+ ].filter(Boolean).join(" "),
379
+ children: [
380
+ /* @__PURE__ */ jsxs3("div", { className: "wf-node__header", children: [
381
+ /* @__PURE__ */ jsx3("span", { className: "wf-node__icon", "aria-hidden": true, children: meta.iconGlyph }),
382
+ /* @__PURE__ */ jsxs3("div", { className: "wf-node__titles", children: [
383
+ /* @__PURE__ */ jsx3("p", { className: "wf-node__kind", children: meta.label }),
384
+ /* @__PURE__ */ jsx3("p", { className: "wf-node__title", children: title })
385
+ ] })
386
+ ] }),
387
+ /* @__PURE__ */ jsxs3("div", { className: "wf-node__body", children: [
388
+ /* @__PURE__ */ jsx3("p", { className: "wf-node__subtitle", children: subtitle2 }),
389
+ /* @__PURE__ */ jsx3(StatusBadge, { issueCount, needsApproval: needsApproval && issueCount === 0 })
390
+ ] }),
391
+ children
392
+ ]
393
+ }
394
+ );
395
+ }
396
+
397
+ // src/canvas/nodes/DecisionNode.tsx
398
+ import { Handle, Position } from "@xyflow/react";
399
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
400
+ function DecisionNode({ data }) {
401
+ const { canvasNode: node, selected, issueCount = 0 } = data;
402
+ if (node.kind !== "DECISION") {
403
+ return null;
404
+ }
405
+ const d = node.data;
406
+ const meta = NODE_KIND_META.DECISION;
407
+ const title = d.name?.trim() || meta.label;
408
+ const handleClass = ["wf-handle", "wf-handle--decision", selected ? "wf-handle--selected" : ""].filter(Boolean).join(" ");
409
+ return /* @__PURE__ */ jsxs4("div", { className: "wf-flow-node", children: [
410
+ /* @__PURE__ */ jsx4(Handle, { type: "target", position: Position.Top, className: handleClass }),
411
+ /* @__PURE__ */ jsx4(
412
+ NodeChrome,
413
+ {
414
+ kind: "DECISION",
415
+ variant: "decision",
416
+ title,
417
+ subtitle: d.contextKey?.trim() || meta.description,
418
+ selected,
419
+ issueCount,
420
+ children: /* @__PURE__ */ jsxs4("div", { className: "wf-node__branches", children: [
421
+ d.cases.map((c) => /* @__PURE__ */ jsxs4("div", { className: "wf-node__branch", children: [
422
+ /* @__PURE__ */ jsx4("span", { className: "wf-node__branch-label", children: c.label || c.value }),
423
+ /* @__PURE__ */ jsx4(Handle, { type: "source", position: Position.Right, id: c.value, className: handleClass })
424
+ ] }, c.value)),
425
+ /* @__PURE__ */ jsxs4("div", { className: "wf-node__branch", children: [
426
+ /* @__PURE__ */ jsx4("span", { className: "wf-node__branch-label", children: ACTION_LABELS.otherwiseBranch }),
427
+ /* @__PURE__ */ jsx4(Handle, { type: "source", position: Position.Right, id: "default", className: handleClass })
428
+ ] })
429
+ ] })
430
+ }
431
+ )
432
+ ] });
433
+ }
434
+
435
+ // src/canvas/nodes/LoopNode.tsx
436
+ import { Handle as Handle2, Position as Position2 } from "@xyflow/react";
437
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
438
+ function LoopNode({ data }) {
439
+ const { canvasNode: node, selected, issueCount = 0, loopBodyLabels } = data;
440
+ if (node.kind !== "REPEAT") {
441
+ return null;
442
+ }
443
+ const d = node.data;
444
+ const meta = NODE_KIND_META.REPEAT;
445
+ const title = d.name?.trim() || meta.label;
446
+ const handleClass = ["wf-handle", "wf-handle--loop", selected ? "wf-handle--selected" : ""].filter(Boolean).join(" ");
447
+ return /* @__PURE__ */ jsxs5("div", { className: "wf-flow-node", children: [
448
+ /* @__PURE__ */ jsx5(Handle2, { type: "target", position: Position2.Top, className: handleClass }),
449
+ /* @__PURE__ */ jsx5(
450
+ NodeChrome,
451
+ {
452
+ kind: "REPEAT",
453
+ variant: "loop",
454
+ title,
455
+ subtitle: `${d.strategy} \xB7 up to ${d.maxIterations} iteration${d.maxIterations === 1 ? "" : "s"}`,
456
+ selected,
457
+ issueCount,
458
+ className: "wf-node--loop-wide",
459
+ children: /* @__PURE__ */ jsxs5("div", { className: "wf-node__loop-body", children: [
460
+ /* @__PURE__ */ jsx5("p", { className: "wf-node__loop-body-title", children: ACTION_LABELS.loopBodyTitle }),
461
+ loopBodyLabels && loopBodyLabels.length > 0 ? /* @__PURE__ */ jsx5("ul", { className: "wf-node__loop-body-list", children: loopBodyLabels.map((label, i) => /* @__PURE__ */ jsxs5("li", { className: "wf-node__loop-body-item", children: [
462
+ /* @__PURE__ */ jsx5("span", { className: "wf-node__loop-body-bullet", "aria-hidden": true }),
463
+ /* @__PURE__ */ jsx5("span", { children: label })
464
+ ] }, `${label}-${i}`)) }) : /* @__PURE__ */ jsx5("p", { className: "wf-node__loop-body-empty", children: ACTION_LABELS.loopBodyEmpty })
465
+ ] })
466
+ }
467
+ ),
468
+ /* @__PURE__ */ jsx5(Handle2, { type: "source", position: Position2.Bottom, className: handleClass })
469
+ ] });
470
+ }
471
+
472
+ // src/canvas/nodes/StepNode.tsx
473
+ import { Handle as Handle3, Position as Position3 } from "@xyflow/react";
474
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
475
+ function subtitle(node) {
476
+ switch (node.kind) {
477
+ case "ASK_USER": {
478
+ const q = node.data.question?.trim() ?? "";
479
+ return q.length > 48 ? `${q.slice(0, 48)}\u2026` : q || NODE_KIND_META.ASK_USER.description;
480
+ }
481
+ case "AI_STEP":
482
+ return node.data.agentRef?.trim() || NODE_KIND_META.AI_STEP.description;
483
+ case "AI_DEBATE":
484
+ return node.data.primaryAgentRef?.trim() || NODE_KIND_META.AI_DEBATE.description;
485
+ case "DECISION":
486
+ return node.data.contextKey?.trim() || NODE_KIND_META.DECISION.description;
487
+ case "REPEAT":
488
+ return `${node.data.strategy} \xB7 max ${node.data.maxIterations}`;
489
+ case "REUSE_WORKFLOW":
490
+ return node.data.workflowRef?.trim() || NODE_KIND_META.REUSE_WORKFLOW.description;
491
+ case "LOAD_RESOURCE":
492
+ return node.data.resourcePath?.trim() || NODE_KIND_META.LOAD_RESOURCE.description;
493
+ case "STOP":
494
+ return node.data.reason?.trim() || NODE_KIND_META.STOP.description;
495
+ case "RETRY":
496
+ return NODE_KIND_META.RETRY.description;
497
+ case "SAVE_RESULT":
498
+ return node.data.resultName?.trim() || NODE_KIND_META.SAVE_RESULT.description;
499
+ default:
500
+ return "";
501
+ }
502
+ }
503
+ function StepNode({ data }) {
504
+ const { canvasNode: node, selected, issueCount = 0, needsApproval = false } = data;
505
+ const meta = NODE_KIND_META[node.kind];
506
+ const title = node.data.name?.trim() || meta.label;
507
+ return /* @__PURE__ */ jsxs6("div", { className: "wf-flow-node", children: [
508
+ /* @__PURE__ */ jsx6(Handle3, { type: "target", position: Position3.Top, className: ["wf-handle", selected ? "wf-handle--selected" : ""].filter(Boolean).join(" ") }),
509
+ /* @__PURE__ */ jsx6(
510
+ NodeChrome,
511
+ {
512
+ kind: node.kind,
513
+ variant: "step",
514
+ title,
515
+ subtitle: subtitle(node),
516
+ selected,
517
+ issueCount,
518
+ needsApproval
519
+ }
520
+ ),
521
+ /* @__PURE__ */ jsx6(Handle3, { type: "source", position: Position3.Bottom, className: ["wf-handle", selected ? "wf-handle--selected" : ""].filter(Boolean).join(" ") })
522
+ ] });
523
+ }
524
+
525
+ // src/canvas/WorkflowCanvas.tsx
526
+ import {
527
+ Background,
528
+ BackgroundVariant,
529
+ Controls,
530
+ MarkerType,
531
+ MiniMap,
532
+ NodeToolbar,
533
+ Position as Position4,
534
+ ReactFlow,
535
+ ReactFlowProvider,
536
+ applyEdgeChanges,
537
+ applyNodeChanges,
538
+ useNodesInitialized,
539
+ useReactFlow
540
+ } from "@xyflow/react";
541
+ import { useCallback, useEffect, useMemo } from "react";
542
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
543
+ var SNAP = 16;
544
+ function nodeTransition(node) {
545
+ const data = node.data;
546
+ return data.transition ?? null;
547
+ }
548
+ function nodeNeedsApproval(node) {
549
+ return nodeTransition(node) === "HUMAN_APPROVAL";
550
+ }
551
+ function isStarterCanvas(model) {
552
+ if (model.nodes.length !== 1 || model.edges.length > 0) {
553
+ return false;
554
+ }
555
+ const only = model.nodes[0];
556
+ return only?.kind === "ASK_USER";
557
+ }
558
+ function toFlowNodes(model, selectedId, issueCountByBackendStepId) {
559
+ return model.nodes.map((n) => {
560
+ const loopBodyLabels = n.kind === "REPEAT" ? model.nodes.filter((c) => c.parentNode === n.id).map((c) => c.data.name?.trim() ? c.data.name.trim() : NODE_KIND_META[c.kind].label) : void 0;
561
+ const issueCount = n.backendStepId ? issueCountByBackendStepId[n.backendStepId] ?? 0 : 0;
562
+ return {
563
+ id: n.id,
564
+ type: n.kind === "DECISION" ? "decision" : n.kind === "REPEAT" ? "loop" : "step",
565
+ position: n.position,
566
+ parentId: n.parentNode,
567
+ extent: n.parentNode ? "parent" : void 0,
568
+ data: {
569
+ canvasNode: n,
570
+ selected: selectedId === n.id,
571
+ loopBodyLabels,
572
+ issueCount,
573
+ needsApproval: nodeNeedsApproval(n)
574
+ },
575
+ draggable: true
576
+ };
577
+ });
578
+ }
579
+ function toFlowEdges(model) {
580
+ const byId = new Map(model.nodes.map((n) => [n.id, n]));
581
+ return model.edges.map((e) => {
582
+ const source = byId.get(e.source);
583
+ return {
584
+ id: e.id,
585
+ source: e.source,
586
+ target: e.target,
587
+ sourceHandle: e.sourceHandle ?? void 0,
588
+ type: "flow",
589
+ data: { transition: source ? nodeTransition(source) : null },
590
+ markerEnd: {
591
+ type: MarkerType.ArrowClosed,
592
+ width: 16,
593
+ height: 16,
594
+ color: "var(--builder-color-edge)"
595
+ }
596
+ };
597
+ });
598
+ }
599
+ var nodeTypes = { step: StepNode, decision: DecisionNode, loop: LoopNode };
600
+ var edgeTypes = { flow: FlowEdge };
601
+ function WorkflowCanvasInner({
602
+ model,
603
+ onModelChange,
604
+ onSelectNode,
605
+ selectedId,
606
+ onAppend,
607
+ issueCountByBackendStepId
608
+ }) {
609
+ const { screenToFlowPosition, setCenter } = useReactFlow();
610
+ const nodesInitialized = useNodesInitialized();
611
+ const showStarterHint = isStarterCanvas(model);
612
+ const flowNodes = useMemo(
613
+ () => toFlowNodes(model, selectedId, issueCountByBackendStepId),
614
+ [issueCountByBackendStepId, model, selectedId]
615
+ );
616
+ const flowEdges = useMemo(() => toFlowEdges(model), [model]);
617
+ useEffect(() => {
618
+ if (!selectedId || !nodesInitialized) {
619
+ return;
620
+ }
621
+ const node = flowNodes.find((entry) => entry.id === selectedId);
622
+ if (!node) {
623
+ return;
624
+ }
625
+ const x = node.position.x + 120;
626
+ const y = node.position.y + 60;
627
+ void setCenter(x, y, { zoom: 1.05, duration: 250 });
628
+ }, [flowNodes, nodesInitialized, selectedId, setCenter]);
629
+ const onNodesChange = useCallback(
630
+ (changes) => {
631
+ const nextFlow = applyNodeChanges(changes, flowNodes);
632
+ const ids = new Set(nextFlow.map((flowNode) => flowNode.id));
633
+ const nextNodes = model.nodes.filter((n) => ids.has(n.id)).map((n) => {
634
+ const fn = nextFlow.find((flowNode) => flowNode.id === n.id);
635
+ return {
636
+ ...n,
637
+ position: fn.position,
638
+ parentNode: fn.parentId
639
+ };
640
+ });
641
+ onModelChange({ ...model, nodes: nextNodes });
642
+ },
643
+ [flowNodes, model, onModelChange]
644
+ );
645
+ const onEdgesChange = useCallback(
646
+ (changes) => {
647
+ const nextFlow = applyEdgeChanges(changes, flowEdges);
648
+ onModelChange({
649
+ ...model,
650
+ edges: nextFlow.map((e) => ({
651
+ id: e.id,
652
+ source: e.source,
653
+ target: e.target,
654
+ sourceHandle: e.sourceHandle ?? null,
655
+ label: null
656
+ }))
657
+ });
658
+ },
659
+ [flowEdges, model, onModelChange]
660
+ );
661
+ const onNodesDelete = useCallback(
662
+ (deletedNodes) => {
663
+ if (deletedNodes.length === 0) {
664
+ return;
665
+ }
666
+ const deletedIds = new Set(deletedNodes.map((node) => node.id));
667
+ onModelChange({
668
+ ...model,
669
+ nodes: model.nodes.filter((node) => !deletedIds.has(node.id)),
670
+ edges: model.edges.filter((edge) => !deletedIds.has(edge.source) && !deletedIds.has(edge.target))
671
+ });
672
+ if (selectedId && deletedIds.has(selectedId)) {
673
+ onSelectNode(null);
674
+ }
675
+ },
676
+ [model, onModelChange, onSelectNode, selectedId]
677
+ );
678
+ const onEdgesDelete = useCallback(
679
+ (deletedEdges) => {
680
+ if (deletedEdges.length === 0) {
681
+ return;
682
+ }
683
+ const deletedIds = new Set(deletedEdges.map((edge) => edge.id));
684
+ onModelChange({
685
+ ...model,
686
+ edges: model.edges.filter((edge) => !deletedIds.has(edge.id))
687
+ });
688
+ },
689
+ [model, onModelChange]
690
+ );
691
+ const deleteSelectedNode = useCallback(() => {
692
+ if (!selectedId) {
693
+ return;
694
+ }
695
+ onModelChange({
696
+ ...model,
697
+ nodes: model.nodes.filter((node) => node.id !== selectedId),
698
+ edges: model.edges.filter((edge) => edge.source !== selectedId && edge.target !== selectedId)
699
+ });
700
+ onSelectNode(null);
701
+ }, [model, onModelChange, onSelectNode, selectedId]);
702
+ const onConnect = useCallback(
703
+ (conn) => {
704
+ if (!conn.source || !conn.target) {
705
+ return;
706
+ }
707
+ const id = `e-${conn.source}-${conn.target}-${conn.sourceHandle ?? "src"}`;
708
+ if (model.edges.some(
709
+ (e) => e.source === conn.source && e.target === conn.target && (e.sourceHandle ?? "") === (conn.sourceHandle ?? "")
710
+ )) {
711
+ return;
712
+ }
713
+ onModelChange({
714
+ ...model,
715
+ edges: [
716
+ ...model.edges,
717
+ {
718
+ id,
719
+ source: conn.source,
720
+ target: conn.target,
721
+ sourceHandle: conn.sourceHandle ?? null,
722
+ label: null
723
+ }
724
+ ]
725
+ });
726
+ },
727
+ [model, onModelChange]
728
+ );
729
+ const onDragOver = useCallback((e) => {
730
+ e.preventDefault();
731
+ e.dataTransfer.dropEffect = "copy";
732
+ }, []);
733
+ const onDrop = useCallback(
734
+ (e) => {
735
+ e.preventDefault();
736
+ const kind = e.dataTransfer.getData("application/agentforge-node-kind");
737
+ if (!kind) {
738
+ return;
739
+ }
740
+ const pos = screenToFlowPosition({ x: e.clientX, y: e.clientY });
741
+ onAppend(kind, pos);
742
+ },
743
+ [onAppend, screenToFlowPosition]
744
+ );
745
+ const startNode = model.nodes.find((n) => n.id === model.startNodeId) ?? model.nodes[0];
746
+ return /* @__PURE__ */ jsxs7("div", { className: "wf-canvas", onDragOver, onDrop, children: [
747
+ /* @__PURE__ */ jsxs7(
748
+ ReactFlow,
749
+ {
750
+ className: "wf-canvas__flow",
751
+ nodes: flowNodes,
752
+ edges: flowEdges,
753
+ nodeTypes,
754
+ edgeTypes,
755
+ onNodesChange,
756
+ onEdgesChange,
757
+ onNodesDelete,
758
+ onEdgesDelete,
759
+ onConnect,
760
+ onNodeClick: (_event, flowNode) => onSelectNode(flowNode.id),
761
+ onPaneClick: () => onSelectNode(null),
762
+ snapToGrid: true,
763
+ snapGrid: [SNAP, SNAP],
764
+ fitView: true,
765
+ panOnDrag: true,
766
+ zoomOnPinch: true,
767
+ panOnScroll: false,
768
+ zoomOnDoubleClick: true,
769
+ proOptions: { hideAttribution: true },
770
+ children: [
771
+ selectedId ? /* @__PURE__ */ jsx7(NodeToolbar, { nodeId: selectedId, isVisible: true, position: Position4.Top, align: "end", offset: 10, children: /* @__PURE__ */ jsx7(
772
+ "button",
773
+ {
774
+ type: "button",
775
+ className: "wf-button wf-button--icon wf-button--secondary",
776
+ "aria-label": ACTION_LABELS.deleteNode,
777
+ title: ACTION_LABELS.deleteNode,
778
+ onClick: deleteSelectedNode,
779
+ children: "\xD7"
780
+ }
781
+ ) }) : null,
782
+ /* @__PURE__ */ jsx7(Background, { variant: BackgroundVariant.Dots, gap: SNAP, size: 1, color: "var(--builder-color-canvas-dot)" }),
783
+ /* @__PURE__ */ jsx7(Controls, { className: "wf-canvas__controls" }),
784
+ /* @__PURE__ */ jsx7(MiniMap, { pannable: true, zoomable: true, className: "wf-canvas__minimap" })
785
+ ]
786
+ }
787
+ ),
788
+ showStarterHint && startNode ? /* @__PURE__ */ jsx7("div", { className: "wf-canvas__starter-overlay", children: /* @__PURE__ */ jsxs7("div", { className: "wf-canvas__starter-card", children: [
789
+ /* @__PURE__ */ jsx7(
790
+ EmptyState,
791
+ {
792
+ icon: /* @__PURE__ */ jsx7("span", { className: "wf-canvas__starter-icon", "aria-hidden": true, children: "\u25CE" }),
793
+ title: BUILDER_COPY.startHere,
794
+ description: NODE_KIND_META.ASK_USER.description,
795
+ primaryAction: {
796
+ label: ACTION_LABELS.selectStartStep,
797
+ onClick: () => onSelectNode(startNode.id)
798
+ },
799
+ secondaryAction: {
800
+ label: BUILDER_COPY.useTemplate,
801
+ onClick: () => {
802
+ }
803
+ }
804
+ }
805
+ ),
806
+ /* @__PURE__ */ jsx7("p", { className: "wf-canvas__starter-hint", children: /* @__PURE__ */ jsx7("button", { type: "button", className: "wf-link-button", onClick: () => {
807
+ }, children: BUILDER_COPY.templateHint }) })
808
+ ] }) }) : null
809
+ ] });
810
+ }
811
+ function WorkflowCanvas({ issueCountByBackendStepId = {}, ...props }) {
812
+ return /* @__PURE__ */ jsx7(ReactFlowProvider, { children: /* @__PURE__ */ jsx7(WorkflowCanvasInner, { ...props, issueCountByBackendStepId }) });
813
+ }
814
+
815
+ // src/guided/GuidedStepper.tsx
816
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
817
+ function GuidedStepper({ stages, activeIndex, onStageAction }) {
818
+ return /* @__PURE__ */ jsx8("section", { className: "wf-guided-stepper wf-panel", children: /* @__PURE__ */ jsx8("ol", { className: "wf-guided-stepper__list", children: stages.map((stage, index) => {
819
+ const isActive = index === activeIndex && !stage.complete;
820
+ return /* @__PURE__ */ jsxs8("li", { className: "wf-guided-stepper__item", children: [
821
+ /* @__PURE__ */ jsx8(
822
+ "span",
823
+ {
824
+ className: [
825
+ "wf-guided-stepper__marker",
826
+ stage.complete ? "wf-guided-stepper__marker--done" : "",
827
+ isActive ? "wf-guided-stepper__marker--active" : ""
828
+ ].filter(Boolean).join(" "),
829
+ "aria-hidden": true,
830
+ children: stage.complete ? "\u2713" : "\u25CB"
831
+ }
832
+ ),
833
+ /* @__PURE__ */ jsx8("div", { className: "wf-guided-stepper__label", children: /* @__PURE__ */ jsxs8(
834
+ "span",
835
+ {
836
+ className: [
837
+ "wf-guided-stepper__text",
838
+ stage.complete ? "wf-guided-stepper__text--done" : ""
839
+ ].filter(Boolean).join(" "),
840
+ children: [
841
+ index + 1,
842
+ ". ",
843
+ stage.label
844
+ ]
845
+ }
846
+ ) }),
847
+ isActive && stage.actionLabel && onStageAction ? /* @__PURE__ */ jsx8(
848
+ "button",
849
+ {
850
+ type: "button",
851
+ className: "wf-button wf-button--secondary wf-guided-stepper__action",
852
+ onClick: () => onStageAction(index),
853
+ children: stage.actionLabel
854
+ }
855
+ ) : null
856
+ ] }, stage.label);
857
+ }) }) });
858
+ }
859
+
860
+ // src/model/mapper.ts
861
+ import { customAlphabet } from "nanoid";
862
+ var nanoid = customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789", 10);
863
+ function findStep(workflow, stepId) {
864
+ const t = stepId.trim();
865
+ return workflow.steps.find((s) => s.stepId.trim() === t);
866
+ }
867
+ function retryPolicyNone() {
868
+ return {
869
+ allowRetry: false,
870
+ allowRetryFromPrevious: false,
871
+ allowAgentSwap: false,
872
+ allowPromptOverride: false,
873
+ maxAttempts: 0
874
+ };
875
+ }
876
+ function retryPolicySimple(maxAttempts) {
877
+ return {
878
+ allowRetry: true,
879
+ allowRetryFromPrevious: false,
880
+ allowAgentSwap: false,
881
+ allowPromptOverride: false,
882
+ maxAttempts
883
+ };
884
+ }
885
+ function exportStepJson(workflow, step) {
886
+ return serializeStepExecutable(workflow, step);
887
+ }
888
+ function serializeStepExecutable(workflow, step) {
889
+ const cm = step.contextMapping ?? { inputKeys: [], outputKeys: [] };
890
+ const body = {
891
+ kind: "STEP",
892
+ stepId: step.stepId,
893
+ name: step.name,
894
+ behaviour: serializeBehaviour(workflow, step),
895
+ contextMapping: {
896
+ inputKeys: cm.inputKeys,
897
+ outputKeys: cm.outputKeys
898
+ }
899
+ };
900
+ if (step.stepPrompt?.trim()) {
901
+ body.stepPrompt = step.stepId;
902
+ }
903
+ return body;
904
+ }
905
+ function serializeBehaviour(workflow, step) {
906
+ switch (step.behaviourType) {
907
+ case "INPUT": {
908
+ const config = step.config;
909
+ return {
910
+ type: "INPUT",
911
+ artifactId: config.artifactId,
912
+ transition: config.transition
913
+ };
914
+ }
915
+ case "AGENT": {
916
+ const config = step.config;
917
+ const max = config.maxRetries ?? 0;
918
+ return {
919
+ type: "AGENT",
920
+ agentRef: config.agentRef,
921
+ transition: config.transition,
922
+ retryPolicy: max > 0 ? retryPolicySimple(max) : retryPolicyNone()
923
+ };
924
+ }
925
+ case "SPAR": {
926
+ const config = step.config;
927
+ return {
928
+ type: "SPAR",
929
+ agentRef: config.agentRef,
930
+ transition: config.transition,
931
+ retryPolicy: retryPolicyNone(),
932
+ sparConfig: {
933
+ challengerAgentId: config.challengerAgentId,
934
+ maxRounds: config.maxRounds,
935
+ resolutionPrompt: config.resolutionPrompt
936
+ }
937
+ };
938
+ }
939
+ case "BRANCH": {
940
+ const config = step.config;
941
+ const branches = {};
942
+ for (const [value, targetId] of Object.entries(config.branches)) {
943
+ const target = findStep(workflow, targetId);
944
+ if (!target) {
945
+ throw new Error(`Branch target missing: ${targetId}`);
946
+ }
947
+ branches[value] = serializeStepExecutable(workflow, target);
948
+ }
949
+ const def = findStep(workflow, config.defaultBranch);
950
+ if (!def) {
951
+ throw new Error(`Default branch target missing: ${config.defaultBranch}`);
952
+ }
953
+ return {
954
+ type: "BRANCH",
955
+ contextKey: config.contextKey,
956
+ branches,
957
+ defaultBranch: serializeStepExecutable(workflow, def)
958
+ };
959
+ }
960
+ case "WORKFLOW_BEHAVIOUR": {
961
+ const config = step.config;
962
+ return {
963
+ type: "WORKFLOW",
964
+ workflowRef: config.workflowRef,
965
+ transition: config.transition
966
+ };
967
+ }
968
+ case "RESOURCE": {
969
+ const config = step.config;
970
+ return {
971
+ type: "RESOURCE",
972
+ resourcePath: config.resourcePath,
973
+ contextKey: config.contextKey,
974
+ transition: config.transition
975
+ };
976
+ }
977
+ case "FAIL": {
978
+ const config = step.config;
979
+ return { type: "FAIL", reason: config.reason };
980
+ }
981
+ case "RETRY_PREVIOUS": {
982
+ const config = step.config;
983
+ const fallbackStep = config.fallback.trim() ? findStep(workflow, config.fallback) : void 0;
984
+ const fallback = fallbackStep != null ? serializeStepExecutable(workflow, fallbackStep) : serializeStepExecutable(workflow, {
985
+ stepId: `${step.stepId}-fallback-missing`,
986
+ name: "Missing fallback",
987
+ behaviourType: "FAIL",
988
+ config: { reason: "Retry fallback step was not configured." }
989
+ });
990
+ return {
991
+ type: "RETRY_PREVIOUS",
992
+ retryStepId: config.retryStepId,
993
+ retryMode: config.retryMode,
994
+ maxAttempts: config.maxAttempts,
995
+ fallback
996
+ };
997
+ }
998
+ }
999
+ }
1000
+ function newStepId(prefix) {
1001
+ return `${prefix}-${nanoid()}`;
1002
+ }
1003
+ function defaultNodeData(kind) {
1004
+ switch (kind) {
1005
+ case "ASK_USER":
1006
+ return {
1007
+ name: "",
1008
+ question: "",
1009
+ artifactItems: [
1010
+ { id: newStepId("item"), type: "TEXT", label: "Response", key: "value", required: true }
1011
+ ]
1012
+ };
1013
+ case "AI_STEP":
1014
+ return {
1015
+ name: "",
1016
+ agentRef: "",
1017
+ instructions: "",
1018
+ transition: "AUTO",
1019
+ maxRetries: 0
1020
+ };
1021
+ case "AI_DEBATE":
1022
+ return {
1023
+ name: "",
1024
+ primaryAgentRef: "",
1025
+ challengerAgentRef: "",
1026
+ maxRounds: 2,
1027
+ resolutionPrompt: "",
1028
+ transition: "AUTO"
1029
+ };
1030
+ case "DECISION":
1031
+ return {
1032
+ name: "",
1033
+ contextKey: "",
1034
+ cases: [{ label: "Yes", value: "yes", targetNodeId: "" }],
1035
+ defaultTargetNodeId: ""
1036
+ };
1037
+ case "REPEAT":
1038
+ return {
1039
+ name: "",
1040
+ strategy: "FIXED_COUNT",
1041
+ maxIterations: 3,
1042
+ maxIterationsAction: "AWAIT_USER",
1043
+ bodyNodeIds: []
1044
+ };
1045
+ case "SAVE_RESULT":
1046
+ return { name: "", resultName: "" };
1047
+ case "REUSE_WORKFLOW":
1048
+ return { name: "", workflowRef: "", transition: "AUTO" };
1049
+ case "LOAD_RESOURCE":
1050
+ return { name: "", resourcePath: "/schema/", resultName: "", transition: "AUTO" };
1051
+ case "STOP":
1052
+ return { name: "", reason: "" };
1053
+ case "RETRY":
1054
+ return {
1055
+ name: "",
1056
+ targetNodeId: "",
1057
+ maxAttempts: 3,
1058
+ fallbackTargetNodeId: "",
1059
+ retryMode: "SINGLE_STEP"
1060
+ };
1061
+ }
1062
+ }
1063
+ function nodeById(model) {
1064
+ return new Map(model.nodes.map((n) => [n.id, n]));
1065
+ }
1066
+ function outgoing(edges, source, sourceHandle) {
1067
+ return edges.filter((e) => e.source === source && (sourceHandle == null ? true : e.sourceHandle === sourceHandle));
1068
+ }
1069
+ function loopBodyNodeIds(model) {
1070
+ const ids = /* @__PURE__ */ new Set();
1071
+ for (const n of model.nodes) {
1072
+ if (n.parentNode) {
1073
+ const parent = model.nodes.find((p) => p.id === n.parentNode);
1074
+ if (parent?.kind === "REPEAT") {
1075
+ ids.add(n.id);
1076
+ }
1077
+ }
1078
+ }
1079
+ return ids;
1080
+ }
1081
+ function collectBranchNestedIds(model) {
1082
+ const nested = /* @__PURE__ */ new Set();
1083
+ for (const n of model.nodes) {
1084
+ if (n.kind !== "DECISION") {
1085
+ continue;
1086
+ }
1087
+ const d = n.data;
1088
+ const mergeId = d.defaultTargetNodeId?.trim();
1089
+ if (!mergeId) {
1090
+ continue;
1091
+ }
1092
+ const visit = (start) => {
1093
+ const q = [start];
1094
+ const seen = /* @__PURE__ */ new Set();
1095
+ while (q.length) {
1096
+ const cur = q.pop();
1097
+ if (cur === mergeId || seen.has(cur)) {
1098
+ continue;
1099
+ }
1100
+ seen.add(cur);
1101
+ nested.add(cur);
1102
+ for (const e of outgoing(model.edges, cur)) {
1103
+ q.push(e.target);
1104
+ }
1105
+ }
1106
+ };
1107
+ for (const c of d.cases) {
1108
+ if (c.targetNodeId.trim()) {
1109
+ visit(c.targetNodeId.trim());
1110
+ }
1111
+ }
1112
+ }
1113
+ return nested;
1114
+ }
1115
+ function topoMainNodeOrder(model, skip) {
1116
+ const start = model.startNodeId;
1117
+ if (!start || skip.has(start)) {
1118
+ return [];
1119
+ }
1120
+ const nodes = model.nodes.filter((n) => !skip.has(n.id) && !n.parentNode);
1121
+ const ids = new Set(nodes.map((n) => n.id));
1122
+ const indeg = /* @__PURE__ */ new Map();
1123
+ for (const id of ids) {
1124
+ indeg.set(id, 0);
1125
+ }
1126
+ const adj = /* @__PURE__ */ new Map();
1127
+ for (const id of ids) {
1128
+ adj.set(id, []);
1129
+ }
1130
+ for (const e of model.edges) {
1131
+ if (!ids.has(e.source) || !ids.has(e.target)) {
1132
+ continue;
1133
+ }
1134
+ indeg.set(e.target, (indeg.get(e.target) ?? 0) + 1);
1135
+ adj.get(e.source).push(e.target);
1136
+ }
1137
+ const q = [];
1138
+ for (const [id, d] of indeg) {
1139
+ if (d === 0) {
1140
+ q.push(id);
1141
+ }
1142
+ }
1143
+ if (!q.includes(start)) {
1144
+ q.unshift(start);
1145
+ }
1146
+ const out = [];
1147
+ const seen = /* @__PURE__ */ new Set();
1148
+ while (q.length) {
1149
+ const cur = q.shift();
1150
+ if (seen.has(cur)) {
1151
+ continue;
1152
+ }
1153
+ seen.add(cur);
1154
+ out.push(cur);
1155
+ for (const nxt of adj.get(cur) ?? []) {
1156
+ const next = (indeg.get(nxt) ?? 0) - 1;
1157
+ indeg.set(nxt, next);
1158
+ if (next === 0) {
1159
+ q.push(nxt);
1160
+ }
1161
+ }
1162
+ }
1163
+ for (const id of ids) {
1164
+ if (!seen.has(id)) {
1165
+ out.push(id);
1166
+ }
1167
+ }
1168
+ return out;
1169
+ }
1170
+ function artifactFromAskUser(node, artifactId) {
1171
+ const d = node.data;
1172
+ const items = (d.artifactItems ?? []).map((it) => ({
1173
+ id: it.key?.trim() || it.id,
1174
+ type: it.type,
1175
+ label: it.label,
1176
+ required: it.required,
1177
+ options: it.options
1178
+ }));
1179
+ return { id: artifactId, items };
1180
+ }
1181
+ function canvasNodeToStep(node, ctx) {
1182
+ const base = (name) => ({
1183
+ stepId: ctx.stepId,
1184
+ name: name || "Step",
1185
+ behaviourType: "INPUT",
1186
+ config: { artifactId: "", transition: "AUTO" },
1187
+ stepPrompt: "",
1188
+ contextMapping: { inputKeys: [], outputKeys: [] }
1189
+ });
1190
+ switch (node.kind) {
1191
+ case "ASK_USER": {
1192
+ const d = node.data;
1193
+ const artifactId = ctx.artifactIdForAsk ?? node.id;
1194
+ return {
1195
+ stepId: ctx.stepId,
1196
+ name: d.name || NODE_LABELS.ASK_USER,
1197
+ behaviourType: "INPUT",
1198
+ config: { artifactId, transition: "AUTO" },
1199
+ stepPrompt: "",
1200
+ contextMapping: { inputKeys: [], outputKeys: [] }
1201
+ };
1202
+ }
1203
+ case "AI_STEP": {
1204
+ const d = node.data;
1205
+ return {
1206
+ stepId: ctx.stepId,
1207
+ name: d.name || NODE_LABELS.AI_STEP,
1208
+ behaviourType: "AGENT",
1209
+ config: {
1210
+ agentRef: d.agentRef,
1211
+ transition: d.transition,
1212
+ maxRetries: d.maxRetries
1213
+ },
1214
+ stepPrompt: d.instructions,
1215
+ contextMapping: { inputKeys: [], outputKeys: [] }
1216
+ };
1217
+ }
1218
+ case "AI_DEBATE": {
1219
+ const d = node.data;
1220
+ return {
1221
+ stepId: ctx.stepId,
1222
+ name: d.name || NODE_LABELS.AI_DEBATE,
1223
+ behaviourType: "SPAR",
1224
+ config: {
1225
+ agentRef: d.primaryAgentRef,
1226
+ challengerAgentId: d.challengerAgentRef,
1227
+ maxRounds: d.maxRounds,
1228
+ resolutionPrompt: d.resolutionPrompt,
1229
+ transition: d.transition
1230
+ },
1231
+ stepPrompt: "",
1232
+ contextMapping: { inputKeys: [], outputKeys: [] }
1233
+ };
1234
+ }
1235
+ case "DECISION": {
1236
+ const d = node.data;
1237
+ const branches = {};
1238
+ for (const c of d.cases) {
1239
+ if (c.value.trim() && c.targetNodeId.trim()) {
1240
+ branches[c.value.trim()] = c.targetNodeId.trim();
1241
+ }
1242
+ }
1243
+ return {
1244
+ stepId: ctx.stepId,
1245
+ name: d.name || "Decision",
1246
+ behaviourType: "BRANCH",
1247
+ config: {
1248
+ contextKey: d.contextKey,
1249
+ branches,
1250
+ defaultBranch: d.defaultTargetNodeId.trim()
1251
+ },
1252
+ stepPrompt: "",
1253
+ contextMapping: { inputKeys: [], outputKeys: [] }
1254
+ };
1255
+ }
1256
+ case "REUSE_WORKFLOW": {
1257
+ const d = node.data;
1258
+ return {
1259
+ stepId: ctx.stepId,
1260
+ name: d.name || NODE_LABELS.REUSE_WORKFLOW,
1261
+ behaviourType: "WORKFLOW_BEHAVIOUR",
1262
+ config: { workflowRef: d.workflowRef, transition: d.transition },
1263
+ stepPrompt: "",
1264
+ contextMapping: { inputKeys: [], outputKeys: [] }
1265
+ };
1266
+ }
1267
+ case "LOAD_RESOURCE": {
1268
+ const d = node.data;
1269
+ return {
1270
+ stepId: ctx.stepId,
1271
+ name: d.name || NODE_LABELS.LOAD_RESOURCE,
1272
+ behaviourType: "RESOURCE",
1273
+ config: {
1274
+ resourcePath: d.resourcePath,
1275
+ contextKey: d.resultName,
1276
+ transition: d.transition
1277
+ },
1278
+ stepPrompt: "",
1279
+ contextMapping: { inputKeys: [], outputKeys: [] }
1280
+ };
1281
+ }
1282
+ case "STOP": {
1283
+ const d = node.data;
1284
+ return {
1285
+ stepId: ctx.stepId,
1286
+ name: d.name || NODE_LABELS.STOP,
1287
+ behaviourType: "FAIL",
1288
+ config: { reason: d.reason },
1289
+ stepPrompt: "",
1290
+ contextMapping: { inputKeys: [], outputKeys: [] }
1291
+ };
1292
+ }
1293
+ case "RETRY": {
1294
+ const d = node.data;
1295
+ return {
1296
+ stepId: ctx.stepId,
1297
+ name: d.name || NODE_LABELS.RETRY,
1298
+ behaviourType: "RETRY_PREVIOUS",
1299
+ config: {
1300
+ retryStepId: d.targetNodeId,
1301
+ retryMode: d.retryMode,
1302
+ maxAttempts: d.maxAttempts,
1303
+ fallback: d.fallbackTargetNodeId
1304
+ },
1305
+ stepPrompt: "",
1306
+ contextMapping: { inputKeys: [], outputKeys: [] }
1307
+ };
1308
+ }
1309
+ case "SAVE_RESULT":
1310
+ case "REPEAT":
1311
+ return base(node.data.name);
1312
+ default:
1313
+ return base("Step");
1314
+ }
1315
+ }
1316
+ function buildCanvasToStepIdMap(model) {
1317
+ const map = /* @__PURE__ */ new Map();
1318
+ const prefixFor = {
1319
+ ASK_USER: "ask-user",
1320
+ AI_STEP: "ai-step",
1321
+ AI_DEBATE: "ai-debate",
1322
+ DECISION: "decision",
1323
+ REPEAT: "repeat",
1324
+ SAVE_RESULT: "save-result",
1325
+ REUSE_WORKFLOW: "reuse-wf",
1326
+ LOAD_RESOURCE: "load-res",
1327
+ STOP: "stop",
1328
+ RETRY: "retry"
1329
+ };
1330
+ for (const n of model.nodes) {
1331
+ map.set(n.id, n.backendStepId ?? newStepId(prefixFor[n.kind] ?? "step"));
1332
+ }
1333
+ return map;
1334
+ }
1335
+ function buildBlueprintJsonForRepeat(model, repeatNode, canvasToStepId, artifacts) {
1336
+ const d = repeatNode.data;
1337
+ const byId = nodeById(model);
1338
+ const ordered = (d.bodyNodeIds?.length ? d.bodyNodeIds : model.nodes.filter((n) => n.parentNode === repeatNode.id).map((n) => n.id)).map((id) => byId.get(id)).filter((n) => Boolean(n));
1339
+ const innerEditable = [];
1340
+ for (const inner of ordered) {
1341
+ const sid = canvasToStepId.get(inner.id) ?? newStepId("inner");
1342
+ let artifactIdForAsk;
1343
+ if (inner.kind === "ASK_USER") {
1344
+ artifactIdForAsk = `artifact-${sid}`;
1345
+ artifacts[artifactIdForAsk] = artifactFromAskUser(inner, artifactIdForAsk);
1346
+ }
1347
+ innerEditable.push(canvasNodeToStep(inner, { stepId: sid, artifactIdForAsk }));
1348
+ }
1349
+ const wfInner = {
1350
+ id: "inline",
1351
+ name: "",
1352
+ description: "",
1353
+ steps: innerEditable,
1354
+ artifacts
1355
+ };
1356
+ const innerSteps = innerEditable.map((s) => exportStepJson(wfInner, s));
1357
+ const loopConfig = {
1358
+ terminationStrategy: d.strategy,
1359
+ maxIterations: d.maxIterations,
1360
+ maxIterationsAction: d.maxIterationsAction
1361
+ };
1362
+ if (d.strategy === "FOR_EACH" && d.forEachKey?.trim()) {
1363
+ loopConfig.forEachContextKey = d.forEachKey.trim();
1364
+ }
1365
+ if (d.strategy === "EVALUATOR" && d.evaluatorAgentId?.trim()) {
1366
+ loopConfig.evaluatorAgentId = d.evaluatorAgentId.trim();
1367
+ }
1368
+ const blueprintId = canvasToStepId.get(repeatNode.id) ?? newStepId("repeat");
1369
+ return {
1370
+ kind: "BLUEPRINT",
1371
+ blueprintId,
1372
+ name: d.name || "Repeat",
1373
+ behaviour: {
1374
+ loopConfig,
1375
+ transition: "AUTO"
1376
+ },
1377
+ steps: innerSteps
1378
+ };
1379
+ }
1380
+ function canvasToWorkflow(model) {
1381
+ const canvasToStepId = buildCanvasToStepIdMap(model);
1382
+ const loopBodies = loopBodyNodeIds(model);
1383
+ const branchNested = collectBranchNestedIds(model);
1384
+ const skipForMainTopo = /* @__PURE__ */ new Set([...loopBodies, ...branchNested]);
1385
+ const mainOrder = topoMainNodeOrder(model, skipForMainTopo);
1386
+ const artifacts = { ...model.artifacts };
1387
+ const steps = [];
1388
+ const blueprintBodies = { ...model.blueprints };
1389
+ const topLevelSchedule = [];
1390
+ const pushStepForNode = (node) => {
1391
+ const sid = canvasToStepId.get(node.id);
1392
+ let artifactIdForAsk;
1393
+ if (node.kind === "ASK_USER") {
1394
+ artifactIdForAsk = `artifact-${sid}`;
1395
+ artifacts[artifactIdForAsk] = artifactFromAskUser(node, artifactIdForAsk);
1396
+ }
1397
+ const st = canvasNodeToStep(node, { stepId: sid, artifactIdForAsk });
1398
+ if (node.kind === "SAVE_RESULT") {
1399
+ const prev = steps[steps.length - 1];
1400
+ const d = node.data;
1401
+ if (prev && d.resultName.trim()) {
1402
+ prev.contextMapping = {
1403
+ inputKeys: prev.contextMapping?.inputKeys ?? [],
1404
+ outputKeys: [d.resultName.trim()]
1405
+ };
1406
+ }
1407
+ return;
1408
+ }
1409
+ steps.push(st);
1410
+ };
1411
+ for (const nid of mainOrder) {
1412
+ const node = model.nodes.find((n) => n.id === nid);
1413
+ if (!node) {
1414
+ continue;
1415
+ }
1416
+ if (node.kind === "REPEAT") {
1417
+ const bpId = canvasToStepId.get(node.id);
1418
+ blueprintBodies[bpId] = buildBlueprintJsonForRepeat(model, node, canvasToStepId, artifacts);
1419
+ topLevelSchedule.push({ kind: "BLUEPRINT_REF", blueprintId: bpId });
1420
+ continue;
1421
+ }
1422
+ if (node.kind === "SAVE_RESULT") {
1423
+ pushStepForNode(node);
1424
+ continue;
1425
+ }
1426
+ const idx = steps.length;
1427
+ pushStepForNode(node);
1428
+ topLevelSchedule.push({ kind: "STEP", stepIndex: idx });
1429
+ }
1430
+ for (const nid of branchNested) {
1431
+ const node = model.nodes.find((n) => n.id === nid);
1432
+ if (!node || node.kind === "SAVE_RESULT") {
1433
+ continue;
1434
+ }
1435
+ pushStepForNode(node);
1436
+ }
1437
+ for (const st of steps) {
1438
+ if (st.behaviourType === "BRANCH") {
1439
+ const c = st.config;
1440
+ const mapBranches = {};
1441
+ for (const [k, targetCanvas] of Object.entries(c.branches)) {
1442
+ const tid = canvasToStepId.get(targetCanvas);
1443
+ mapBranches[k] = tid ?? targetCanvas;
1444
+ }
1445
+ const defTid = canvasToStepId.get(c.defaultBranch.trim()) ?? c.defaultBranch.trim();
1446
+ st.config = { ...c, branches: mapBranches, defaultBranch: defTid };
1447
+ }
1448
+ if (st.behaviourType === "RETRY_PREVIOUS") {
1449
+ const c = st.config;
1450
+ const fb = canvasToStepId.get(c.fallback.trim()) ?? c.fallback.trim();
1451
+ const rt = canvasToStepId.get(c.retryStepId.trim()) ?? c.retryStepId.trim();
1452
+ st.config = { ...c, retryStepId: rt, fallback: fb };
1453
+ }
1454
+ }
1455
+ return {
1456
+ id: model.workflowId.trim(),
1457
+ name: model.workflowName.trim(),
1458
+ description: model.description.trim(),
1459
+ steps,
1460
+ artifacts,
1461
+ blueprintBodies,
1462
+ topLevelSchedule
1463
+ };
1464
+ }
1465
+ function workflowToCanvas(workflow) {
1466
+ const nodes = [];
1467
+ const edges = [];
1468
+ let y = 0;
1469
+ const gap = 120;
1470
+ const stepToNode = /* @__PURE__ */ new Map();
1471
+ workflow.steps.forEach((s, i) => {
1472
+ stepToNode.set(s.stepId, `n-${i}`);
1473
+ });
1474
+ for (let i = 0; i < workflow.steps.length; i++) {
1475
+ const st = workflow.steps[i];
1476
+ const cid = `n-${i}`;
1477
+ let kind = "AI_STEP";
1478
+ let data = defaultNodeData("AI_STEP");
1479
+ if (st.behaviourType === "INPUT") {
1480
+ kind = "ASK_USER";
1481
+ const cfg = st.config;
1482
+ const art = workflow.artifacts[cfg.artifactId];
1483
+ data = {
1484
+ name: st.name,
1485
+ question: "",
1486
+ artifactItems: art?.items.map((it) => ({
1487
+ id: it.id,
1488
+ type: it.type,
1489
+ label: it.label,
1490
+ key: it.id,
1491
+ required: it.required,
1492
+ options: it.options
1493
+ })) ?? []
1494
+ };
1495
+ } else if (st.behaviourType === "AGENT") {
1496
+ const cfg = st.config;
1497
+ data = {
1498
+ name: st.name,
1499
+ agentRef: cfg.agentRef,
1500
+ instructions: st.stepPrompt ?? "",
1501
+ transition: cfg.transition,
1502
+ maxRetries: cfg.maxRetries ?? 0
1503
+ };
1504
+ } else if (st.behaviourType === "BRANCH") {
1505
+ kind = "DECISION";
1506
+ const cfg = st.config;
1507
+ data = {
1508
+ name: st.name,
1509
+ contextKey: cfg.contextKey,
1510
+ cases: Object.entries(cfg.branches).map(([value, targetNodeId]) => ({
1511
+ label: value,
1512
+ value,
1513
+ targetNodeId: stepToNode.get(targetNodeId) ?? ""
1514
+ })),
1515
+ defaultTargetNodeId: stepToNode.get(cfg.defaultBranch) ?? ""
1516
+ };
1517
+ } else if (st.behaviourType === "WORKFLOW_BEHAVIOUR") {
1518
+ kind = "REUSE_WORKFLOW";
1519
+ const cfg = st.config;
1520
+ data = { name: st.name, workflowRef: cfg.workflowRef, transition: cfg.transition };
1521
+ } else if (st.behaviourType === "RESOURCE") {
1522
+ kind = "LOAD_RESOURCE";
1523
+ const cfg = st.config;
1524
+ data = {
1525
+ name: st.name,
1526
+ resourcePath: cfg.resourcePath,
1527
+ resultName: cfg.contextKey,
1528
+ transition: cfg.transition
1529
+ };
1530
+ } else if (st.behaviourType === "FAIL") {
1531
+ kind = "STOP";
1532
+ const cfg = st.config;
1533
+ data = { name: st.name, reason: cfg.reason };
1534
+ } else if (st.behaviourType === "RETRY_PREVIOUS") {
1535
+ kind = "RETRY";
1536
+ const cfg = st.config;
1537
+ data = {
1538
+ name: st.name,
1539
+ targetNodeId: stepToNode.get(cfg.retryStepId) ?? cfg.retryStepId,
1540
+ maxAttempts: cfg.maxAttempts,
1541
+ fallbackTargetNodeId: stepToNode.get(cfg.fallback) ?? cfg.fallback,
1542
+ retryMode: cfg.retryMode
1543
+ };
1544
+ } else if (st.behaviourType === "SPAR") {
1545
+ kind = "AI_DEBATE";
1546
+ const cfg = st.config;
1547
+ data = {
1548
+ name: st.name,
1549
+ primaryAgentRef: cfg.agentRef,
1550
+ challengerAgentRef: cfg.challengerAgentId,
1551
+ maxRounds: cfg.maxRounds,
1552
+ resolutionPrompt: cfg.resolutionPrompt,
1553
+ transition: cfg.transition
1554
+ };
1555
+ }
1556
+ nodes.push({ id: cid, backendStepId: st.stepId, kind, position: { x: 200, y }, data });
1557
+ y += gap;
1558
+ if (i > 0) {
1559
+ edges.push({
1560
+ id: `e-${i - 1}-${i}`,
1561
+ source: `n-${i - 1}`,
1562
+ target: cid
1563
+ });
1564
+ }
1565
+ }
1566
+ const blueprintKeys = workflow.blueprintBodies ? Object.keys(workflow.blueprintBodies) : [];
1567
+ const unsupported = blueprintKeys.length > 0;
1568
+ const unsupportedReasons = unsupported ? [
1569
+ `${blueprintKeys.length} blueprint definition(s) are kept for export. The loop body is read-only here until nested editing is supported.`
1570
+ ] : void 0;
1571
+ return {
1572
+ workflowId: workflow.id,
1573
+ workflowName: workflow.name,
1574
+ description: workflow.description,
1575
+ startNodeId: nodes[0]?.id ?? null,
1576
+ nodes,
1577
+ edges,
1578
+ artifacts: workflow.artifacts,
1579
+ blueprints: {},
1580
+ unsupported,
1581
+ unsupportedReasons
1582
+ };
1583
+ }
1584
+
1585
+ // src/hooks/useCanvasState.ts
1586
+ import { useCallback as useCallback2, useState } from "react";
1587
+ function createInitialCanvasModel() {
1588
+ const backendStepId = newStepId("ask-user");
1589
+ const id = `c-${backendStepId}`;
1590
+ return {
1591
+ workflowId: "",
1592
+ workflowName: "",
1593
+ description: "",
1594
+ startNodeId: id,
1595
+ nodes: [
1596
+ {
1597
+ id,
1598
+ backendStepId,
1599
+ kind: "ASK_USER",
1600
+ position: { x: 80, y: 120 },
1601
+ data: defaultNodeData("ASK_USER")
1602
+ }
1603
+ ],
1604
+ edges: [],
1605
+ artifacts: {},
1606
+ blueprints: {}
1607
+ };
1608
+ }
1609
+ function useCanvasState(initialModel) {
1610
+ const [model, setModel] = useState(initialModel);
1611
+ const [selectedId, setSelectedId] = useState(null);
1612
+ const [isDirty2, setIsDirty] = useState(false);
1613
+ const markDirty = useCallback2(() => {
1614
+ setIsDirty(true);
1615
+ }, []);
1616
+ const markClean = useCallback2(() => {
1617
+ setIsDirty(false);
1618
+ }, []);
1619
+ const setModelFromLoad = useCallback2((next) => {
1620
+ setModel((current) => typeof next === "function" ? next(current) : next);
1621
+ setIsDirty(false);
1622
+ }, []);
1623
+ const setModelDirty = useCallback2((next) => {
1624
+ setModel((current) => typeof next === "function" ? next(current) : next);
1625
+ setIsDirty(true);
1626
+ }, []);
1627
+ const updateNodeData = useCallback2(
1628
+ (id, partial) => {
1629
+ setModelDirty((m) => ({
1630
+ ...m,
1631
+ nodes: m.nodes.map((n) => {
1632
+ if (n.id !== id) {
1633
+ return n;
1634
+ }
1635
+ return { ...n, data: { ...n.data, ...partial } };
1636
+ })
1637
+ }));
1638
+ },
1639
+ [setModelDirty]
1640
+ );
1641
+ const appendNode = useCallback2(
1642
+ (kind, position) => {
1643
+ const prefix = {
1644
+ ASK_USER: "ask-user",
1645
+ AI_STEP: "ai-step",
1646
+ AI_DEBATE: "ai-debate",
1647
+ DECISION: "decision",
1648
+ REPEAT: "repeat",
1649
+ SAVE_RESULT: "save-result",
1650
+ REUSE_WORKFLOW: "reuse-wf",
1651
+ LOAD_RESOURCE: "load-res",
1652
+ STOP: "stop",
1653
+ RETRY: "retry"
1654
+ };
1655
+ const backendStepId = newStepId(prefix[kind]);
1656
+ const id = `c-${backendStepId}`;
1657
+ const node = {
1658
+ id,
1659
+ backendStepId,
1660
+ kind,
1661
+ position,
1662
+ data: defaultNodeData(kind)
1663
+ };
1664
+ setModelDirty((m) => ({
1665
+ ...m,
1666
+ nodes: [...m.nodes, node],
1667
+ startNodeId: m.startNodeId ?? id
1668
+ }));
1669
+ setSelectedId(id);
1670
+ },
1671
+ [setModelDirty]
1672
+ );
1673
+ return {
1674
+ model,
1675
+ setModel: setModelDirty,
1676
+ setModelFromLoad,
1677
+ selectedId,
1678
+ setSelectedId,
1679
+ isDirty: isDirty2,
1680
+ markDirty,
1681
+ markClean,
1682
+ updateNodeData,
1683
+ appendNode
1684
+ };
1685
+ }
1686
+
1687
+ // src/hooks/useBuilderMode.ts
1688
+ import { useCallback as useCallback3, useState as useState2 } from "react";
1689
+ var BUILDER_MODE_KEY = "agentforge_builder_mode";
1690
+ function isUntouchedStarter(model) {
1691
+ if (model.nodes.length !== 1) {
1692
+ return false;
1693
+ }
1694
+ const starter = model.nodes[0];
1695
+ if (starter.kind !== "ASK_USER") {
1696
+ return false;
1697
+ }
1698
+ return !model.workflowId.trim() && !model.workflowName.trim() && !model.description.trim() && !starter.data.name.trim() && !starter.data.question.trim();
1699
+ }
1700
+ function readStoredBuilderMode() {
1701
+ if (typeof window === "undefined") {
1702
+ return null;
1703
+ }
1704
+ const value = window.localStorage.getItem(BUILDER_MODE_KEY);
1705
+ if (value === "guided" || value === "advanced") {
1706
+ return value;
1707
+ }
1708
+ return null;
1709
+ }
1710
+ function useBuilderMode(model, isNewRoute) {
1711
+ const [mode, setMode] = useState2(() => {
1712
+ const stored = readStoredBuilderMode();
1713
+ if (stored) {
1714
+ return stored;
1715
+ }
1716
+ if (isNewRoute && isUntouchedStarter(model)) {
1717
+ return "guided";
1718
+ }
1719
+ return "advanced";
1720
+ });
1721
+ const setAndPersistMode = useCallback3((nextMode) => {
1722
+ setMode(nextMode);
1723
+ if (typeof window !== "undefined") {
1724
+ window.localStorage.setItem(BUILDER_MODE_KEY, nextMode);
1725
+ }
1726
+ }, []);
1727
+ return { mode, setMode: setAndPersistMode };
1728
+ }
1729
+
1730
+ // src/validation/validateWorkflow.ts
1731
+ var ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1732
+ function hasOptions(item) {
1733
+ if (item.type !== "SINGLE_CHOICE" && item.type !== "MULTI_CHOICE") {
1734
+ return true;
1735
+ }
1736
+ return Boolean(item.options?.some((o) => o.trim().length > 0));
1737
+ }
1738
+ function collectNestedOnlyStepIds(workflow) {
1739
+ const nested = /* @__PURE__ */ new Set();
1740
+ for (const step of workflow.steps) {
1741
+ if (step.behaviourType === "BRANCH") {
1742
+ const c = step.config;
1743
+ Object.values(c.branches).forEach((id) => {
1744
+ if (id.trim()) {
1745
+ nested.add(id.trim());
1746
+ }
1747
+ });
1748
+ if (c.defaultBranch.trim()) {
1749
+ nested.add(c.defaultBranch.trim());
1750
+ }
1751
+ }
1752
+ if (step.behaviourType === "RETRY_PREVIOUS") {
1753
+ const c = step.config;
1754
+ if (c.fallback.trim()) {
1755
+ nested.add(c.fallback.trim());
1756
+ }
1757
+ }
1758
+ }
1759
+ return nested;
1760
+ }
1761
+ function validateEditableWorkflow(workflow) {
1762
+ const errors = {
1763
+ workflow: {},
1764
+ steps: {},
1765
+ artifacts: {},
1766
+ global: []
1767
+ };
1768
+ if (!workflow.id.trim()) {
1769
+ errors.workflow.id = "Workflow id is required.";
1770
+ } else if (!ID_PATTERN.test(workflow.id.trim())) {
1771
+ errors.workflow.id = "Workflow id must use lowercase letters, numbers, and dashes.";
1772
+ }
1773
+ if (!workflow.name.trim()) {
1774
+ errors.workflow.name = "Workflow name is required.";
1775
+ }
1776
+ if (workflow.steps.length === 0) {
1777
+ errors.global.push("At least one step is required.");
1778
+ }
1779
+ if (workflow.topLevelSchedule && workflow.topLevelSchedule.length > 0) {
1780
+ const nestedOnly = collectNestedOnlyStepIds(workflow);
1781
+ workflow.topLevelSchedule.forEach((entry, scheduleIndex) => {
1782
+ if (entry.kind === "STEP") {
1783
+ if (!Number.isInteger(entry.stepIndex) || entry.stepIndex < 0 || entry.stepIndex >= workflow.steps.length) {
1784
+ errors.global.push(`Top-level schedule entry ${scheduleIndex}: invalid step index ${entry.stepIndex}.`);
1785
+ } else {
1786
+ const st = workflow.steps[entry.stepIndex];
1787
+ if (st && nestedOnly.has(st.stepId.trim())) {
1788
+ errors.global.push(
1789
+ `Top-level schedule entry ${scheduleIndex}: step "${st.stepId}" is nested-only and cannot appear in the main schedule.`
1790
+ );
1791
+ }
1792
+ }
1793
+ } else if (entry.kind === "BLUEPRINT_REF") {
1794
+ if (!entry.blueprintId?.trim()) {
1795
+ errors.global.push(`Top-level schedule entry ${scheduleIndex}: blueprint id is required.`);
1796
+ }
1797
+ }
1798
+ });
1799
+ }
1800
+ const seenStepIds = /* @__PURE__ */ new Set();
1801
+ workflow.steps.forEach((step, index) => {
1802
+ const stepErrors = {};
1803
+ if (!step.stepId.trim()) {
1804
+ stepErrors.stepId = "Step id is required.";
1805
+ } else if (!ID_PATTERN.test(step.stepId.trim())) {
1806
+ stepErrors.stepId = "Step id must use lowercase letters, numbers, and dashes.";
1807
+ } else if (seenStepIds.has(step.stepId.trim())) {
1808
+ stepErrors.stepId = "Step id must be unique.";
1809
+ } else {
1810
+ seenStepIds.add(step.stepId.trim());
1811
+ }
1812
+ if (!step.name.trim()) {
1813
+ stepErrors.name = "Step name is required.";
1814
+ }
1815
+ if (step.behaviourType === "INPUT") {
1816
+ const config = step.config;
1817
+ if (!config.artifactId.trim()) {
1818
+ stepErrors.artifactId = "Artifact id is required for INPUT steps.";
1819
+ } else if (!workflow.artifacts[config.artifactId]) {
1820
+ stepErrors.artifactId = `Artifact "${config.artifactId}" does not exist.`;
1821
+ }
1822
+ }
1823
+ if (step.behaviourType === "AGENT") {
1824
+ const config = step.config;
1825
+ if (!config.agentRef.trim()) {
1826
+ stepErrors.agentRef = "agentRef is required.";
1827
+ }
1828
+ }
1829
+ if (step.behaviourType === "SPAR") {
1830
+ const config = step.config;
1831
+ if (!config.agentRef.trim()) {
1832
+ stepErrors.agentRef = "agentRef is required.";
1833
+ }
1834
+ if (!config.challengerAgentId.trim()) {
1835
+ stepErrors.challengerAgentId = "challengerAgentId is required.";
1836
+ }
1837
+ if (!config.resolutionPrompt.trim()) {
1838
+ stepErrors.resolutionPrompt = "resolutionPrompt is required.";
1839
+ }
1840
+ if (!Number.isFinite(config.maxRounds) || config.maxRounds < 1) {
1841
+ stepErrors.maxRounds = "maxRounds must be >= 1.";
1842
+ }
1843
+ }
1844
+ if (step.behaviourType === "RESOURCE") {
1845
+ const config = step.config;
1846
+ if (!config.resourcePath.startsWith("/schema/")) {
1847
+ stepErrors.resourcePath = "resourcePath must start with /schema/.";
1848
+ }
1849
+ if (!config.contextKey.trim()) {
1850
+ stepErrors.contextKey = "contextKey is required.";
1851
+ }
1852
+ }
1853
+ if (step.behaviourType === "WORKFLOW_BEHAVIOUR") {
1854
+ const config = step.config;
1855
+ if (!config.workflowRef.trim()) {
1856
+ stepErrors.workflowRef = "workflowRef is required.";
1857
+ }
1858
+ }
1859
+ if (step.behaviourType === "FAIL") {
1860
+ const config = step.config;
1861
+ if (!config.reason.trim()) {
1862
+ stepErrors.reason = "reason is required.";
1863
+ }
1864
+ }
1865
+ if (step.behaviourType === "BRANCH") {
1866
+ const config = step.config;
1867
+ if (!config.contextKey.trim()) {
1868
+ stepErrors.contextKey = "Branch context key is required.";
1869
+ }
1870
+ const resolveId = (id) => workflow.steps.some((s) => s.stepId.trim() === id.trim());
1871
+ for (const [key, targetId] of Object.entries(config.branches)) {
1872
+ if (!targetId.trim()) {
1873
+ stepErrors[`branch-${key}`] = "Branch target step id is required.";
1874
+ } else if (!resolveId(targetId)) {
1875
+ stepErrors[`branch-${key}`] = `No step with id "${targetId}".`;
1876
+ }
1877
+ }
1878
+ if (!config.defaultBranch.trim()) {
1879
+ stepErrors.defaultBranch = "Default branch target step id is required.";
1880
+ } else if (!resolveId(config.defaultBranch)) {
1881
+ stepErrors.defaultBranch = `No step with id "${config.defaultBranch}".`;
1882
+ }
1883
+ }
1884
+ if (step.behaviourType === "RETRY_PREVIOUS") {
1885
+ const config = step.config;
1886
+ if (!config.retryStepId.trim()) {
1887
+ stepErrors.retryStepId = "retryStepId is required.";
1888
+ }
1889
+ if (!Number.isFinite(config.maxAttempts) || config.maxAttempts < 1) {
1890
+ stepErrors.maxAttempts = "maxAttempts must be >= 1.";
1891
+ }
1892
+ if (config.fallback.trim() && !workflow.steps.some((s) => s.stepId.trim() === config.fallback.trim())) {
1893
+ stepErrors.fallback = `No step with id "${config.fallback}".`;
1894
+ }
1895
+ }
1896
+ if (Object.keys(stepErrors).length > 0) {
1897
+ errors.steps[index] = stepErrors;
1898
+ }
1899
+ });
1900
+ for (const [artifactId, artifact] of Object.entries(workflow.artifacts)) {
1901
+ const artifactErrors = {};
1902
+ if (!artifactId.trim()) {
1903
+ artifactErrors.id = "Artifact id is required.";
1904
+ }
1905
+ if (artifact.items.length === 0) {
1906
+ artifactErrors.items = "Artifact must contain at least one item.";
1907
+ }
1908
+ artifact.items.forEach((item, index) => {
1909
+ if (!item.id.trim()) {
1910
+ artifactErrors[`item-${index}-id`] = "Item id is required.";
1911
+ }
1912
+ if (!item.label.trim()) {
1913
+ artifactErrors[`item-${index}-label`] = "Item label is required.";
1914
+ }
1915
+ if (!hasOptions(item)) {
1916
+ artifactErrors[`item-${index}-options`] = "Choice items must include at least one option.";
1917
+ }
1918
+ });
1919
+ if (Object.keys(artifactErrors).length > 0) {
1920
+ errors.artifacts[artifactId] = artifactErrors;
1921
+ }
1922
+ }
1923
+ return errors;
1924
+ }
1925
+ function toValidationResult(errors) {
1926
+ const issues = [];
1927
+ for (const [field, message] of Object.entries(errors.workflow)) {
1928
+ if (message) {
1929
+ issues.push({ path: `workflow.${field}`, message, severity: "error" });
1930
+ }
1931
+ }
1932
+ for (const [index, stepErrors] of Object.entries(errors.steps)) {
1933
+ for (const [field, message] of Object.entries(stepErrors)) {
1934
+ if (message) {
1935
+ issues.push({ path: `steps[${index}].${field}`, message, severity: "error" });
1936
+ }
1937
+ }
1938
+ }
1939
+ for (const [artifactId, artifactErrors] of Object.entries(errors.artifacts)) {
1940
+ for (const [field, message] of Object.entries(artifactErrors)) {
1941
+ if (message) {
1942
+ issues.push({ path: `artifacts.${artifactId}.${field}`, message, severity: "error" });
1943
+ }
1944
+ }
1945
+ }
1946
+ for (const message of errors.global) {
1947
+ issues.push({ path: "global", message, severity: "error" });
1948
+ }
1949
+ return { valid: issues.length === 0, issues };
1950
+ }
1951
+ function validateWorkflow(draft) {
1952
+ return toValidationResult(validateEditableWorkflow(draft));
1953
+ }
1954
+ function validateWorkflowEditor(draft) {
1955
+ return validateEditableWorkflow(draft);
1956
+ }
1957
+
1958
+ // src/hooks/useWorkflowDraft.ts
1959
+ import { useCallback as useCallback4, useState as useState3 } from "react";
1960
+ function parseStepIdFromMessage(message) {
1961
+ const m = message.match(/stepId[=:'\s]+([a-z0-9-]+)/i) ?? message.match(/step\s+['"]?([a-z0-9-]+)['"]?/i);
1962
+ return m?.[1];
1963
+ }
1964
+ function validationToIssues(model, validation) {
1965
+ const issues = [];
1966
+ for (const message of Object.values(validation.workflow)) {
1967
+ if (message) {
1968
+ issues.push({ code: "workflow", message, stepId: parseStepIdFromMessage(message) });
1969
+ }
1970
+ }
1971
+ for (const [idx, stepErrors] of Object.entries(validation.steps)) {
1972
+ for (const message of Object.values(stepErrors)) {
1973
+ if (message) {
1974
+ issues.push({
1975
+ code: "field",
1976
+ message: `Step ${idx}: ${message}`,
1977
+ stepId: model.nodes[Number(idx)]?.backendStepId
1978
+ });
1979
+ }
1980
+ }
1981
+ }
1982
+ for (const message of validation.global) {
1983
+ issues.push({ code: "global", message, stepId: void 0 });
1984
+ }
1985
+ return issues;
1986
+ }
1987
+ function useWorkflowDraft() {
1988
+ const [issues, setIssues] = useState3([]);
1989
+ const buildFromCanvas = useCallback4((model) => {
1990
+ const workflow = canvasToWorkflow(model);
1991
+ const validation = validateWorkflowEditor(workflow);
1992
+ return { workflow, validation };
1993
+ }, []);
1994
+ const validateOnly = useCallback4(
1995
+ (model) => {
1996
+ const { workflow, validation } = buildFromCanvas(model);
1997
+ const nextIssues = validationToIssues(model, validation);
1998
+ setIssues(nextIssues);
1999
+ return { workflow, validation, valid: nextIssues.length === 0 };
2000
+ },
2001
+ [buildFromCanvas]
2002
+ );
2003
+ return {
2004
+ issues,
2005
+ setIssues,
2006
+ buildFromCanvas,
2007
+ validateOnly
2008
+ };
2009
+ }
2010
+
2011
+ // src/inspector/StepConfigPanel.tsx
2012
+ import { useMemo as useMemo2 } from "react";
2013
+ import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
2014
+ function TransitionField({
2015
+ value,
2016
+ onChange
2017
+ }) {
2018
+ return /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2019
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", title: ACTION_LABELS.approvalHelp, children: ACTION_LABELS.approvalField }),
2020
+ /* @__PURE__ */ jsxs9("select", { className: "wf-input wf-select", value, onChange: (e) => onChange(e.target.value), children: [
2021
+ /* @__PURE__ */ jsx9("option", { value: "AUTO", children: ACTION_LABELS.approvalAutomatic }),
2022
+ /* @__PURE__ */ jsx9("option", { value: "HUMAN_REVIEW", children: ACTION_LABELS.approvalReview }),
2023
+ /* @__PURE__ */ jsx9("option", { value: "HUMAN_APPROVAL", children: ACTION_LABELS.approvalRequired })
2024
+ ] })
2025
+ ] });
2026
+ }
2027
+ function Section({ title, children }) {
2028
+ return /* @__PURE__ */ jsxs9("section", { className: "wf-inspector__section", children: [
2029
+ /* @__PURE__ */ jsx9("h3", { className: "wf-inspector__section-title", children: title }),
2030
+ children
2031
+ ] });
2032
+ }
2033
+ function StepConfigPanel({
2034
+ model,
2035
+ selectedId,
2036
+ mode,
2037
+ onClose,
2038
+ onUpdateNodeData,
2039
+ agentCatalog
2040
+ }) {
2041
+ const node = useMemo2(() => model.nodes.find((n) => n.id === selectedId) ?? null, [model.nodes, selectedId]);
2042
+ const hasAgentCatalog = Boolean(agentCatalog && agentCatalog.length > 0);
2043
+ const otherNodes = useMemo2(
2044
+ () => model.nodes.filter((n) => n.id !== node?.id).map((n) => ({ id: n.id, label: n.data.name || NODE_KIND_META[n.kind].label })),
2045
+ [model.nodes, node]
2046
+ );
2047
+ if (!node || !selectedId) {
2048
+ return null;
2049
+ }
2050
+ const title = NODE_KIND_META[node.kind].label;
2051
+ const insideLoopBody = Boolean(node.parentNode) && model.nodes.some((p) => p.id === node.parentNode && p.kind === "REPEAT");
2052
+ const behaviorOpen = mode === "advanced";
2053
+ const advancedOpen = mode === "advanced";
2054
+ return /* @__PURE__ */ jsxs9("aside", { className: "wf-panel wf-inspector", role: "dialog", "aria-label": title, children: [
2055
+ /* @__PURE__ */ jsxs9("header", { className: "wf-panel__header wf-inspector__header", children: [
2056
+ /* @__PURE__ */ jsx9("h2", { className: "wf-panel__title", children: title }),
2057
+ /* @__PURE__ */ jsx9(
2058
+ "button",
2059
+ {
2060
+ type: "button",
2061
+ className: "wf-button wf-button--ghost wf-button--icon",
2062
+ "aria-label": ACTION_LABELS.configureStepClose,
2063
+ onClick: onClose,
2064
+ children: "\xD7"
2065
+ }
2066
+ )
2067
+ ] }),
2068
+ /* @__PURE__ */ jsxs9("div", { className: "wf-panel__body wf-inspector__body", children: [
2069
+ insideLoopBody ? /* @__PURE__ */ jsx9("p", { className: "wf-inspector__banner", children: ACTION_LABELS.loopBodyReadOnly }) : null,
2070
+ /* @__PURE__ */ jsxs9("fieldset", { disabled: insideLoopBody, className: "wf-inspector__fieldset", children: [
2071
+ /* @__PURE__ */ jsxs9(Section, { title: ACTION_LABELS.basicsSection, children: [
2072
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2073
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.nameField }),
2074
+ /* @__PURE__ */ jsx9(
2075
+ "input",
2076
+ {
2077
+ className: "wf-input",
2078
+ value: node.data.name,
2079
+ onChange: (e) => onUpdateNodeData(node.id, { name: e.target.value })
2080
+ }
2081
+ )
2082
+ ] }),
2083
+ /* @__PURE__ */ jsxs9("div", { className: "wf-inspector__grid", children: [
2084
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2085
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.typeField }),
2086
+ /* @__PURE__ */ jsx9("input", { className: "wf-input", value: title, disabled: true })
2087
+ ] }),
2088
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2089
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.descriptionField }),
2090
+ /* @__PURE__ */ jsx9("input", { className: "wf-input", value: NODE_KIND_META[node.kind].description, disabled: true })
2091
+ ] })
2092
+ ] })
2093
+ ] }),
2094
+ /* @__PURE__ */ jsxs9(Section, { title: ACTION_LABELS.inputsOutputsSection, children: [
2095
+ node.kind === "ASK_USER" ? /* @__PURE__ */ jsxs9(Fragment2, { children: [
2096
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2097
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.questionField }),
2098
+ /* @__PURE__ */ jsx9(
2099
+ "textarea",
2100
+ {
2101
+ className: "wf-input wf-textarea",
2102
+ value: node.data.question,
2103
+ onChange: (e) => onUpdateNodeData(node.id, { question: e.target.value })
2104
+ }
2105
+ )
2106
+ ] }),
2107
+ /* @__PURE__ */ jsx9("p", { className: "wf-field__hint", children: ACTION_LABELS.formFieldsHint }),
2108
+ node.data.artifactItems.map((item, idx) => /* @__PURE__ */ jsxs9("div", { className: "wf-inspector__card", children: [
2109
+ /* @__PURE__ */ jsx9(
2110
+ "select",
2111
+ {
2112
+ className: "wf-input wf-select",
2113
+ value: item.type,
2114
+ onChange: (e) => {
2115
+ const next = [...node.data.artifactItems];
2116
+ next[idx] = { ...item, type: e.target.value };
2117
+ onUpdateNodeData(node.id, { artifactItems: next });
2118
+ },
2119
+ children: ["TEXT", "TEXT_AREA", "SINGLE_CHOICE", "MULTI_CHOICE", "BOOLEAN", "NUMBER", "DATE"].map(
2120
+ (t) => /* @__PURE__ */ jsx9("option", { value: t, children: t }, t)
2121
+ )
2122
+ }
2123
+ ),
2124
+ /* @__PURE__ */ jsx9(
2125
+ "input",
2126
+ {
2127
+ className: "wf-input",
2128
+ placeholder: "Label",
2129
+ value: item.label,
2130
+ onChange: (e) => {
2131
+ const next = [...node.data.artifactItems];
2132
+ next[idx] = { ...item, label: e.target.value };
2133
+ onUpdateNodeData(node.id, { artifactItems: next });
2134
+ }
2135
+ }
2136
+ ),
2137
+ /* @__PURE__ */ jsx9(
2138
+ "input",
2139
+ {
2140
+ className: "wf-input",
2141
+ placeholder: "Key",
2142
+ value: item.key,
2143
+ onChange: (e) => {
2144
+ const next = [...node.data.artifactItems];
2145
+ next[idx] = { ...item, key: e.target.value };
2146
+ onUpdateNodeData(node.id, { artifactItems: next });
2147
+ }
2148
+ }
2149
+ )
2150
+ ] }, item.id)),
2151
+ /* @__PURE__ */ jsx9(
2152
+ "button",
2153
+ {
2154
+ type: "button",
2155
+ className: "wf-button wf-button--secondary",
2156
+ onClick: () => onUpdateNodeData(node.id, {
2157
+ artifactItems: [
2158
+ ...node.data.artifactItems,
2159
+ {
2160
+ id: `item-${node.data.artifactItems.length}`,
2161
+ type: "TEXT",
2162
+ label: "",
2163
+ key: "",
2164
+ required: false
2165
+ }
2166
+ ]
2167
+ }),
2168
+ children: ACTION_LABELS.addField
2169
+ }
2170
+ )
2171
+ ] }) : null,
2172
+ node.kind === "SAVE_RESULT" ? /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2173
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.resultNameField }),
2174
+ /* @__PURE__ */ jsx9(
2175
+ "input",
2176
+ {
2177
+ className: "wf-input",
2178
+ value: node.data.resultName,
2179
+ onChange: (e) => onUpdateNodeData(node.id, { resultName: e.target.value })
2180
+ }
2181
+ )
2182
+ ] }) : null,
2183
+ node.kind === "LOAD_RESOURCE" ? /* @__PURE__ */ jsxs9(Fragment2, { children: [
2184
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2185
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.resourcePathField }),
2186
+ /* @__PURE__ */ jsx9(
2187
+ "input",
2188
+ {
2189
+ className: "wf-input",
2190
+ value: node.data.resourcePath,
2191
+ onChange: (e) => onUpdateNodeData(node.id, { resourcePath: e.target.value })
2192
+ }
2193
+ )
2194
+ ] }),
2195
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2196
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.resultContextKeyField }),
2197
+ /* @__PURE__ */ jsx9(
2198
+ "input",
2199
+ {
2200
+ className: "wf-input",
2201
+ value: node.data.resultName,
2202
+ onChange: (e) => onUpdateNodeData(node.id, { resultName: e.target.value })
2203
+ }
2204
+ )
2205
+ ] })
2206
+ ] }) : null
2207
+ ] }),
2208
+ /* @__PURE__ */ jsxs9("details", { className: "wf-inspector__details", open: behaviorOpen, children: [
2209
+ /* @__PURE__ */ jsx9("summary", { className: "wf-inspector__details-summary", children: ACTION_LABELS.behaviorSection }),
2210
+ /* @__PURE__ */ jsxs9("div", { className: "wf-inspector__details-body", children: [
2211
+ node.kind === "AI_STEP" ? /* @__PURE__ */ jsxs9(Fragment2, { children: [
2212
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2213
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.agentField }),
2214
+ hasAgentCatalog ? /* @__PURE__ */ jsxs9(
2215
+ "select",
2216
+ {
2217
+ className: "wf-input wf-select",
2218
+ value: node.data.agentRef,
2219
+ onChange: (e) => onUpdateNodeData(node.id, { agentRef: e.target.value }),
2220
+ children: [
2221
+ /* @__PURE__ */ jsx9("option", { value: "", children: ACTION_LABELS.selectPlaceholder }),
2222
+ agentCatalog.map((a) => /* @__PURE__ */ jsx9("option", { value: a.id, children: a.name }, a.id))
2223
+ ]
2224
+ }
2225
+ ) : /* @__PURE__ */ jsx9(
2226
+ "input",
2227
+ {
2228
+ className: "wf-input",
2229
+ value: node.data.agentRef,
2230
+ placeholder: ACTION_LABELS.agentPlaceholder,
2231
+ onChange: (e) => onUpdateNodeData(node.id, { agentRef: e.target.value })
2232
+ }
2233
+ )
2234
+ ] }),
2235
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2236
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.instructionsField }),
2237
+ /* @__PURE__ */ jsx9(
2238
+ "textarea",
2239
+ {
2240
+ className: "wf-input wf-textarea wf-textarea--tall",
2241
+ value: node.data.instructions,
2242
+ onChange: (e) => onUpdateNodeData(node.id, { instructions: e.target.value })
2243
+ }
2244
+ )
2245
+ ] }),
2246
+ /* @__PURE__ */ jsx9(
2247
+ TransitionField,
2248
+ {
2249
+ value: node.data.transition,
2250
+ onChange: (value) => onUpdateNodeData(node.id, { transition: value })
2251
+ }
2252
+ )
2253
+ ] }) : null,
2254
+ node.kind === "AI_DEBATE" ? /* @__PURE__ */ jsx9(DebateForm, { node, agentCatalog, onUpdate: onUpdateNodeData }) : null,
2255
+ node.kind === "DECISION" ? /* @__PURE__ */ jsx9(DecisionForm, { node, otherNodes, onUpdate: onUpdateNodeData }) : null,
2256
+ node.kind === "REPEAT" ? /* @__PURE__ */ jsx9(RepeatForm, { node, onUpdate: onUpdateNodeData }) : null,
2257
+ node.kind === "REUSE_WORKFLOW" ? /* @__PURE__ */ jsxs9(Fragment2, { children: [
2258
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2259
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.reuseWorkflow }),
2260
+ /* @__PURE__ */ jsx9(
2261
+ "input",
2262
+ {
2263
+ className: "wf-input",
2264
+ value: node.data.workflowRef,
2265
+ placeholder: ACTION_LABELS.workflowIdPlaceholder,
2266
+ onChange: (e) => onUpdateNodeData(node.id, { workflowRef: e.target.value })
2267
+ }
2268
+ )
2269
+ ] }),
2270
+ /* @__PURE__ */ jsx9(
2271
+ TransitionField,
2272
+ {
2273
+ value: node.data.transition,
2274
+ onChange: (value) => onUpdateNodeData(node.id, { transition: value })
2275
+ }
2276
+ )
2277
+ ] }) : null,
2278
+ node.kind === "STOP" ? /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2279
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.reasonField }),
2280
+ /* @__PURE__ */ jsx9(
2281
+ "textarea",
2282
+ {
2283
+ className: "wf-input wf-textarea",
2284
+ value: node.data.reason,
2285
+ onChange: (e) => onUpdateNodeData(node.id, { reason: e.target.value })
2286
+ }
2287
+ )
2288
+ ] }) : null,
2289
+ node.kind === "RETRY" ? /* @__PURE__ */ jsxs9(Fragment2, { children: [
2290
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2291
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.targetStepField }),
2292
+ /* @__PURE__ */ jsxs9(
2293
+ "select",
2294
+ {
2295
+ className: "wf-input wf-select",
2296
+ value: node.data.targetNodeId,
2297
+ onChange: (e) => onUpdateNodeData(node.id, { targetNodeId: e.target.value }),
2298
+ children: [
2299
+ /* @__PURE__ */ jsx9("option", { value: "", children: ACTION_LABELS.selectPlaceholder }),
2300
+ otherNodes.map((o) => /* @__PURE__ */ jsx9("option", { value: o.id, children: o.label }, o.id))
2301
+ ]
2302
+ }
2303
+ )
2304
+ ] }),
2305
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2306
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.fallbackTargetField }),
2307
+ /* @__PURE__ */ jsxs9(
2308
+ "select",
2309
+ {
2310
+ className: "wf-input wf-select",
2311
+ value: node.data.fallbackTargetNodeId,
2312
+ onChange: (e) => onUpdateNodeData(node.id, { fallbackTargetNodeId: e.target.value }),
2313
+ children: [
2314
+ /* @__PURE__ */ jsx9("option", { value: "", children: ACTION_LABELS.selectPlaceholder }),
2315
+ otherNodes.map((o) => /* @__PURE__ */ jsx9("option", { value: o.id, children: o.label }, o.id))
2316
+ ]
2317
+ }
2318
+ )
2319
+ ] })
2320
+ ] }) : null
2321
+ ] })
2322
+ ] }),
2323
+ /* @__PURE__ */ jsxs9("details", { className: "wf-inspector__details", open: advancedOpen, children: [
2324
+ /* @__PURE__ */ jsx9("summary", { className: "wf-inspector__details-summary", children: ACTION_LABELS.advancedSection }),
2325
+ /* @__PURE__ */ jsxs9("div", { className: "wf-inspector__details-body", children: [
2326
+ node.kind === "AI_STEP" ? /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2327
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.maxRetriesField }),
2328
+ /* @__PURE__ */ jsx9(
2329
+ "input",
2330
+ {
2331
+ className: "wf-input",
2332
+ type: "number",
2333
+ min: 0,
2334
+ value: node.data.maxRetries,
2335
+ onChange: (e) => onUpdateNodeData(node.id, { maxRetries: Number(e.target.value || 0) })
2336
+ }
2337
+ )
2338
+ ] }) : null,
2339
+ node.kind === "RETRY" ? /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2340
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.maxAttemptsField }),
2341
+ /* @__PURE__ */ jsx9(
2342
+ "input",
2343
+ {
2344
+ className: "wf-input",
2345
+ type: "number",
2346
+ min: 1,
2347
+ value: node.data.maxAttempts,
2348
+ onChange: (e) => onUpdateNodeData(node.id, { maxAttempts: Number(e.target.value || 1) })
2349
+ }
2350
+ )
2351
+ ] }) : null
2352
+ ] })
2353
+ ] })
2354
+ ] })
2355
+ ] })
2356
+ ] });
2357
+ }
2358
+ function DebateForm({
2359
+ node,
2360
+ agentCatalog,
2361
+ onUpdate
2362
+ }) {
2363
+ if (node.kind !== "AI_DEBATE") {
2364
+ return null;
2365
+ }
2366
+ const d = node.data;
2367
+ const hasCatalog = Boolean(agentCatalog && agentCatalog.length > 0);
2368
+ const agentControl = (field) => hasCatalog ? /* @__PURE__ */ jsxs9(
2369
+ "select",
2370
+ {
2371
+ className: "wf-input wf-select",
2372
+ value: d[field],
2373
+ onChange: (e) => onUpdate(node.id, { [field]: e.target.value }),
2374
+ children: [
2375
+ /* @__PURE__ */ jsx9("option", { value: "", children: ACTION_LABELS.selectPlaceholder }),
2376
+ agentCatalog.map((a) => /* @__PURE__ */ jsx9("option", { value: a.id, children: a.name }, a.id))
2377
+ ]
2378
+ }
2379
+ ) : /* @__PURE__ */ jsx9(
2380
+ "input",
2381
+ {
2382
+ className: "wf-input",
2383
+ value: d[field],
2384
+ placeholder: ACTION_LABELS.agentPlaceholder,
2385
+ onChange: (e) => onUpdate(node.id, { [field]: e.target.value })
2386
+ }
2387
+ );
2388
+ return /* @__PURE__ */ jsxs9(Fragment2, { children: [
2389
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2390
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.primaryAgentField }),
2391
+ agentControl("primaryAgentRef")
2392
+ ] }),
2393
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2394
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.challengerAgentField }),
2395
+ agentControl("challengerAgentRef")
2396
+ ] }),
2397
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2398
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.maxRoundsField }),
2399
+ /* @__PURE__ */ jsx9(
2400
+ "input",
2401
+ {
2402
+ className: "wf-input",
2403
+ type: "number",
2404
+ min: 1,
2405
+ value: d.maxRounds,
2406
+ onChange: (e) => onUpdate(node.id, { maxRounds: Number(e.target.value || 1) })
2407
+ }
2408
+ )
2409
+ ] }),
2410
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2411
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.resolutionInstructionsField }),
2412
+ /* @__PURE__ */ jsx9(
2413
+ "textarea",
2414
+ {
2415
+ className: "wf-input wf-textarea",
2416
+ value: d.resolutionPrompt,
2417
+ onChange: (e) => onUpdate(node.id, { resolutionPrompt: e.target.value })
2418
+ }
2419
+ )
2420
+ ] })
2421
+ ] });
2422
+ }
2423
+ function DecisionForm({
2424
+ node,
2425
+ otherNodes,
2426
+ onUpdate
2427
+ }) {
2428
+ if (node.kind !== "DECISION") {
2429
+ return null;
2430
+ }
2431
+ const d = node.data;
2432
+ return /* @__PURE__ */ jsxs9(Fragment2, { children: [
2433
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2434
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.contextKeyField }),
2435
+ /* @__PURE__ */ jsx9(
2436
+ "input",
2437
+ {
2438
+ className: "wf-input",
2439
+ value: d.contextKey,
2440
+ onChange: (e) => onUpdate(node.id, { contextKey: e.target.value })
2441
+ }
2442
+ ),
2443
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__hint", children: ACTION_LABELS.contextKeyHint })
2444
+ ] }),
2445
+ d.cases.map((c, idx) => /* @__PURE__ */ jsxs9("div", { className: "wf-inspector__card", children: [
2446
+ /* @__PURE__ */ jsx9(
2447
+ "input",
2448
+ {
2449
+ className: "wf-input",
2450
+ placeholder: ACTION_LABELS.caseLabelPlaceholder,
2451
+ value: c.label,
2452
+ onChange: (e) => {
2453
+ const next = [...d.cases];
2454
+ next[idx] = { ...c, label: e.target.value };
2455
+ onUpdate(node.id, { cases: next });
2456
+ }
2457
+ }
2458
+ ),
2459
+ /* @__PURE__ */ jsx9(
2460
+ "input",
2461
+ {
2462
+ className: "wf-input",
2463
+ placeholder: ACTION_LABELS.caseValuePlaceholder,
2464
+ value: c.value,
2465
+ onChange: (e) => {
2466
+ const next = [...d.cases];
2467
+ next[idx] = { ...c, value: e.target.value };
2468
+ onUpdate(node.id, { cases: next });
2469
+ }
2470
+ }
2471
+ ),
2472
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2473
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.connectsToField }),
2474
+ /* @__PURE__ */ jsxs9(
2475
+ "select",
2476
+ {
2477
+ className: "wf-input wf-select",
2478
+ value: c.targetNodeId,
2479
+ onChange: (e) => {
2480
+ const next = [...d.cases];
2481
+ next[idx] = { ...c, targetNodeId: e.target.value };
2482
+ onUpdate(node.id, { cases: next });
2483
+ },
2484
+ children: [
2485
+ /* @__PURE__ */ jsx9("option", { value: "", children: ACTION_LABELS.selectPlaceholder }),
2486
+ otherNodes.map((o) => /* @__PURE__ */ jsx9("option", { value: o.id, children: o.label }, o.id))
2487
+ ]
2488
+ }
2489
+ )
2490
+ ] })
2491
+ ] }, `${c.value}-${idx}`)),
2492
+ /* @__PURE__ */ jsx9(
2493
+ "button",
2494
+ {
2495
+ type: "button",
2496
+ className: "wf-button wf-button--secondary",
2497
+ onClick: () => onUpdate(node.id, {
2498
+ cases: [...d.cases, { label: "New case", value: `case-${d.cases.length + 1}`, targetNodeId: "" }]
2499
+ }),
2500
+ children: ACTION_LABELS.addCase
2501
+ }
2502
+ ),
2503
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2504
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.defaultBranchTargetField }),
2505
+ /* @__PURE__ */ jsxs9(
2506
+ "select",
2507
+ {
2508
+ className: "wf-input wf-select",
2509
+ value: d.defaultTargetNodeId,
2510
+ onChange: (e) => onUpdate(node.id, { defaultTargetNodeId: e.target.value }),
2511
+ children: [
2512
+ /* @__PURE__ */ jsx9("option", { value: "", children: ACTION_LABELS.selectPlaceholder }),
2513
+ otherNodes.map((o) => /* @__PURE__ */ jsx9("option", { value: o.id, children: o.label }, o.id))
2514
+ ]
2515
+ }
2516
+ )
2517
+ ] })
2518
+ ] });
2519
+ }
2520
+ function RepeatForm({ node, onUpdate }) {
2521
+ if (node.kind !== "REPEAT") {
2522
+ return null;
2523
+ }
2524
+ const d = node.data;
2525
+ return /* @__PURE__ */ jsxs9(Fragment2, { children: [
2526
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2527
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.loopModeField }),
2528
+ /* @__PURE__ */ jsxs9(
2529
+ "select",
2530
+ {
2531
+ className: "wf-input wf-select",
2532
+ value: d.strategy,
2533
+ onChange: (e) => onUpdate(node.id, { strategy: e.target.value }),
2534
+ children: [
2535
+ /* @__PURE__ */ jsx9("option", { value: "FIXED_COUNT", children: ACTION_LABELS.fixedCountMode }),
2536
+ /* @__PURE__ */ jsx9("option", { value: "FOR_EACH", children: ACTION_LABELS.forEachMode }),
2537
+ /* @__PURE__ */ jsx9("option", { value: "AGENT_SIGNAL", children: ACTION_LABELS.agentSignalMode }),
2538
+ /* @__PURE__ */ jsx9("option", { value: "EVALUATOR", children: ACTION_LABELS.evaluatorMode })
2539
+ ]
2540
+ }
2541
+ )
2542
+ ] }),
2543
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2544
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.maxIterationsField }),
2545
+ /* @__PURE__ */ jsx9(
2546
+ "input",
2547
+ {
2548
+ className: "wf-input",
2549
+ type: "number",
2550
+ min: 1,
2551
+ value: d.maxIterations,
2552
+ onChange: (e) => onUpdate(node.id, { maxIterations: Number(e.target.value || 1) })
2553
+ }
2554
+ )
2555
+ ] }),
2556
+ d.strategy === "FOR_EACH" ? /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2557
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.listContextKeyField }),
2558
+ /* @__PURE__ */ jsx9(
2559
+ "input",
2560
+ {
2561
+ className: "wf-input",
2562
+ value: d.forEachKey ?? "",
2563
+ onChange: (e) => onUpdate(node.id, { forEachKey: e.target.value })
2564
+ }
2565
+ )
2566
+ ] }) : null,
2567
+ d.strategy === "EVALUATOR" ? /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2568
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.evaluatorAgentField }),
2569
+ /* @__PURE__ */ jsx9(
2570
+ "input",
2571
+ {
2572
+ className: "wf-input",
2573
+ value: d.evaluatorAgentId ?? "",
2574
+ onChange: (e) => onUpdate(node.id, { evaluatorAgentId: e.target.value })
2575
+ }
2576
+ )
2577
+ ] }) : null,
2578
+ /* @__PURE__ */ jsxs9("label", { className: "wf-field", children: [
2579
+ /* @__PURE__ */ jsx9("span", { className: "wf-field__label", children: ACTION_LABELS.maxIterationsActionField }),
2580
+ /* @__PURE__ */ jsxs9(
2581
+ "select",
2582
+ {
2583
+ className: "wf-input wf-select",
2584
+ value: d.maxIterationsAction,
2585
+ onChange: (e) => onUpdate(node.id, { maxIterationsAction: e.target.value }),
2586
+ children: [
2587
+ /* @__PURE__ */ jsx9("option", { value: "AWAIT_USER", children: ACTION_LABELS.awaitUserAction }),
2588
+ /* @__PURE__ */ jsx9("option", { value: "FAIL", children: ACTION_LABELS.failAction })
2589
+ ]
2590
+ }
2591
+ )
2592
+ ] })
2593
+ ] });
2594
+ }
2595
+
2596
+ // src/palette/StepPalette.tsx
2597
+ import { useEffect as useEffect2, useState as useState4 } from "react";
2598
+ import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
2599
+ function StepPalette({ mode, onAddStep, defaultCollapsed = true }) {
2600
+ const [expanded, setExpanded] = useState4(!defaultCollapsed);
2601
+ const [advancedOpen, setAdvancedOpen] = useState4(mode === "advanced");
2602
+ const [isMobile, setIsMobile] = useState4(false);
2603
+ const [mobileOpen, setMobileOpen] = useState4(false);
2604
+ useEffect2(() => {
2605
+ setAdvancedOpen(mode === "advanced");
2606
+ }, [mode]);
2607
+ useEffect2(() => {
2608
+ if (typeof window === "undefined") {
2609
+ return;
2610
+ }
2611
+ const media = window.matchMedia("(max-width: 767px)");
2612
+ const update = () => setIsMobile(media.matches);
2613
+ update();
2614
+ media.addEventListener("change", update);
2615
+ return () => media.removeEventListener("change", update);
2616
+ }, []);
2617
+ const renderKind = (kind, compact) => {
2618
+ const meta = NODE_KIND_META[kind];
2619
+ return /* @__PURE__ */ jsxs10(
2620
+ "button",
2621
+ {
2622
+ type: "button",
2623
+ draggable: true,
2624
+ title: meta.description,
2625
+ onDragStart: (e) => {
2626
+ e.dataTransfer.setData("application/agentforge-node-kind", kind);
2627
+ e.dataTransfer.effectAllowed = "copy";
2628
+ },
2629
+ onClick: () => {
2630
+ onAddStep(kind);
2631
+ if (isMobile) {
2632
+ setMobileOpen(false);
2633
+ }
2634
+ },
2635
+ className: ["wf-palette__item", compact ? "wf-palette__item--compact" : ""].filter(Boolean).join(" "),
2636
+ "aria-label": meta.label,
2637
+ children: [
2638
+ /* @__PURE__ */ jsx10("span", { className: "wf-palette__item-icon", "aria-hidden": true, children: meta.iconGlyph }),
2639
+ !compact ? /* @__PURE__ */ jsxs10("span", { className: "wf-palette__item-text", children: [
2640
+ /* @__PURE__ */ jsx10("span", { className: "wf-palette__item-label", children: meta.label }),
2641
+ /* @__PURE__ */ jsx10("span", { className: "wf-palette__item-description", children: meta.description })
2642
+ ] }) : null
2643
+ ]
2644
+ },
2645
+ kind
2646
+ );
2647
+ };
2648
+ const panelContent = /* @__PURE__ */ jsxs10(Fragment3, { children: [
2649
+ /* @__PURE__ */ jsxs10("div", { className: "wf-palette__header", children: [
2650
+ /* @__PURE__ */ jsx10("span", { className: "wf-palette__title", children: BUILDER_COPY.stepLibrary }),
2651
+ !isMobile ? /* @__PURE__ */ jsx10(
2652
+ "button",
2653
+ {
2654
+ type: "button",
2655
+ className: "wf-button wf-button--icon wf-button--ghost",
2656
+ "aria-label": expanded ? BUILDER_COPY.collapsePalette : BUILDER_COPY.expandPalette,
2657
+ onClick: () => setExpanded((v) => !v),
2658
+ children: expanded ? "\u2039" : "\u203A"
2659
+ }
2660
+ ) : null
2661
+ ] }),
2662
+ /* @__PURE__ */ jsxs10("div", { className: "wf-palette__body", children: [
2663
+ /* @__PURE__ */ jsx10("p", { className: "wf-palette__section-label", children: "Common" }),
2664
+ /* @__PURE__ */ jsx10("div", { className: "wf-palette__list", children: LIBRARY_COMMON_KINDS.map((kind) => renderKind(kind, !expanded && !isMobile)) }),
2665
+ expanded || isMobile ? /* @__PURE__ */ jsxs10(Fragment3, { children: [
2666
+ /* @__PURE__ */ jsxs10(
2667
+ "button",
2668
+ {
2669
+ type: "button",
2670
+ className: "wf-palette__advanced-toggle",
2671
+ onClick: () => setAdvancedOpen((o) => !o),
2672
+ children: [
2673
+ /* @__PURE__ */ jsx10("span", { "aria-hidden": true, children: advancedOpen ? "\u25BE" : "\u25B8" }),
2674
+ mode === "guided" ? ACTION_LABELS.advancedStepsGuided : ACTION_LABELS.advancedSteps
2675
+ ]
2676
+ }
2677
+ ),
2678
+ advancedOpen ? /* @__PURE__ */ jsx10("div", { className: "wf-palette__list", children: LIBRARY_ADVANCED_KINDS.map((kind) => renderKind(kind, false)) }) : null
2679
+ ] }) : null
2680
+ ] })
2681
+ ] });
2682
+ if (isMobile) {
2683
+ return /* @__PURE__ */ jsxs10("div", { className: "wf-palette wf-palette--mobile", children: [
2684
+ /* @__PURE__ */ jsxs10(
2685
+ "button",
2686
+ {
2687
+ type: "button",
2688
+ className: "wf-button wf-button--primary wf-palette__mobile-trigger",
2689
+ "aria-label": ACTION_LABELS.addStep,
2690
+ "aria-expanded": mobileOpen,
2691
+ onClick: () => setMobileOpen((o) => !o),
2692
+ children: [
2693
+ "+ ",
2694
+ ACTION_LABELS.addStep
2695
+ ]
2696
+ }
2697
+ ),
2698
+ mobileOpen ? /* @__PURE__ */ jsxs10("div", { className: "wf-palette__mobile-sheet", role: "dialog", "aria-label": BUILDER_COPY.stepLibrary, children: [
2699
+ /* @__PURE__ */ jsx10("p", { className: "wf-palette__mobile-description", children: ACTION_LABELS.chooseStepDescription }),
2700
+ /* @__PURE__ */ jsx10("div", { className: "wf-palette__mobile-content", children: panelContent })
2701
+ ] }) : null
2702
+ ] });
2703
+ }
2704
+ return /* @__PURE__ */ jsxs10(
2705
+ "aside",
2706
+ {
2707
+ className: ["wf-palette", expanded ? "wf-palette--expanded" : "wf-palette--collapsed"].join(" "),
2708
+ onMouseEnter: () => {
2709
+ if (!expanded) {
2710
+ setExpanded(true);
2711
+ }
2712
+ },
2713
+ onMouseLeave: () => {
2714
+ if (defaultCollapsed) {
2715
+ setExpanded(false);
2716
+ }
2717
+ },
2718
+ children: [
2719
+ !expanded ? /* @__PURE__ */ jsx10("div", { className: "wf-palette__collapsed-header", children: /* @__PURE__ */ jsx10(
2720
+ "button",
2721
+ {
2722
+ type: "button",
2723
+ className: "wf-button wf-button--icon wf-button--ghost",
2724
+ "aria-label": BUILDER_COPY.expandPalette,
2725
+ onClick: () => setExpanded(true),
2726
+ children: "\u2630"
2727
+ }
2728
+ ) }) : null,
2729
+ /* @__PURE__ */ jsx10("div", { className: ["wf-palette__panel", !expanded ? "wf-palette__panel--hidden" : ""].filter(Boolean).join(" "), children: panelContent }),
2730
+ !expanded ? /* @__PURE__ */ jsx10("div", { className: "wf-palette__collapsed-list", children: LIBRARY_COMMON_KINDS.map((kind) => renderKind(kind, true)) }) : null
2731
+ ]
2732
+ }
2733
+ );
2734
+ }
2735
+
2736
+ // src/state/useBuilderState.ts
2737
+ import { useMemo as useMemo3, useReducer } from "react";
2738
+
2739
+ // src/api/types.ts
2740
+ function emptyWorkflow() {
2741
+ return {
2742
+ id: "",
2743
+ name: "",
2744
+ description: "",
2745
+ steps: [],
2746
+ artifacts: {}
2747
+ };
2748
+ }
2749
+
2750
+ // src/state/reducer.ts
2751
+ var initialValidation = { valid: true, issues: [] };
2752
+ var initialImportMeta = { source: null, importedAt: null };
2753
+ function createInitialState(workflow = emptyWorkflow()) {
2754
+ return {
2755
+ draft: workflow,
2756
+ baseline: workflow,
2757
+ selection: { nodeId: null, edgeId: null },
2758
+ validation: initialValidation,
2759
+ importMeta: initialImportMeta
2760
+ };
2761
+ }
2762
+ function isDirty(state) {
2763
+ return JSON.stringify(state.draft) !== JSON.stringify(state.baseline);
2764
+ }
2765
+ function builderReducer(state, action) {
2766
+ switch (action.type) {
2767
+ case "SET_DRAFT":
2768
+ return { ...state, draft: action.draft };
2769
+ case "SET_BASELINE":
2770
+ return { ...state, baseline: action.baseline };
2771
+ case "SELECT_NODE":
2772
+ return {
2773
+ ...state,
2774
+ selection: { ...state.selection, nodeId: action.nodeId, edgeId: null }
2775
+ };
2776
+ case "SELECT_EDGE":
2777
+ return {
2778
+ ...state,
2779
+ selection: { ...state.selection, edgeId: action.edgeId, nodeId: null }
2780
+ };
2781
+ case "SET_VALIDATION":
2782
+ return { ...state, validation: action.validation };
2783
+ case "SET_IMPORT_META":
2784
+ return { ...state, importMeta: action.importMeta };
2785
+ default:
2786
+ return state;
2787
+ }
2788
+ }
2789
+
2790
+ // src/state/useBuilderState.ts
2791
+ function useBuilderState(initialWorkflow) {
2792
+ const [state, dispatch] = useReducer(
2793
+ builderReducer,
2794
+ initialWorkflow,
2795
+ (workflow) => createInitialState(workflow)
2796
+ );
2797
+ const dirty = useMemo3(() => isDirty(state), [state]);
2798
+ return { state, dispatch, dirty };
2799
+ }
2800
+
2801
+ // src/validation-ui/ValidationPanel.tsx
2802
+ import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
2803
+ function rewriteIssue(message) {
2804
+ const normalized = message.trim().toLowerCase();
2805
+ const matched = Object.entries(ISSUE_REWRITES).find(([key]) => normalized.includes(key));
2806
+ return matched?.[1] ?? message;
2807
+ }
2808
+ function severityGlyph(issue) {
2809
+ if (issue.code.toLowerCase().includes("warning")) {
2810
+ return "\u26A0";
2811
+ }
2812
+ return "!";
2813
+ }
2814
+ function groupIssues(model, issues) {
2815
+ const grouped = /* @__PURE__ */ new Map();
2816
+ for (const issue of issues) {
2817
+ const stepId = issue.stepId?.trim();
2818
+ if (!stepId) {
2819
+ const fallback = grouped.get("workflow") ?? { key: "workflow", title: "Workflow", issues: [] };
2820
+ fallback.issues.push(issue);
2821
+ grouped.set("workflow", fallback);
2822
+ continue;
2823
+ }
2824
+ const node = model.nodes.find((entry2) => entry2.backendStepId === stepId);
2825
+ const defaultTitle = node ? `${NODE_KIND_META[node.kind].label} \xB7 ${node.data.name?.trim() || "Untitled step"}` : stepId;
2826
+ const entry = grouped.get(stepId) ?? { key: stepId, title: defaultTitle, issues: [] };
2827
+ entry.issues.push(issue);
2828
+ grouped.set(stepId, entry);
2829
+ }
2830
+ return [...grouped.values()];
2831
+ }
2832
+ function Section2({
2833
+ title,
2834
+ groups,
2835
+ onFix
2836
+ }) {
2837
+ if (groups.length === 0) {
2838
+ return null;
2839
+ }
2840
+ return /* @__PURE__ */ jsxs11("div", { className: "wf-validation-panel__section", children: [
2841
+ /* @__PURE__ */ jsx11("h4", { className: "wf-validation-panel__section-title", children: title }),
2842
+ groups.map((group) => /* @__PURE__ */ jsxs11("details", { className: "wf-validation-panel__group", open: true, children: [
2843
+ /* @__PURE__ */ jsx11("summary", { className: "wf-validation-panel__group-summary", children: group.title }),
2844
+ /* @__PURE__ */ jsx11("ul", { className: "wf-validation-panel__issue-list", children: group.issues.map((issue, index) => /* @__PURE__ */ jsxs11("li", { className: "wf-validation-panel__issue", children: [
2845
+ /* @__PURE__ */ jsxs11("div", { className: "wf-validation-panel__issue-content", children: [
2846
+ /* @__PURE__ */ jsx11("span", { className: "wf-validation-panel__issue-glyph", "aria-hidden": true, children: severityGlyph(issue) }),
2847
+ /* @__PURE__ */ jsx11("span", { className: "wf-validation-panel__issue-message", children: rewriteIssue(issue.message) })
2848
+ ] }),
2849
+ /* @__PURE__ */ jsx11(
2850
+ "button",
2851
+ {
2852
+ type: "button",
2853
+ className: "wf-button wf-button--secondary wf-validation-panel__fix-button",
2854
+ disabled: !issue.stepId,
2855
+ onClick: () => {
2856
+ if (!issue.stepId) {
2857
+ console.info("Validation issue has no stepId; cannot focus specific step.", issue);
2858
+ }
2859
+ onFix(issue.stepId);
2860
+ },
2861
+ children: ACTION_LABELS.fixIssue
2862
+ }
2863
+ )
2864
+ ] }, `${group.key}-${issue.code}-${index}`)) })
2865
+ ] }, group.key))
2866
+ ] });
2867
+ }
2868
+ function ValidationPanel({ model, clientIssues, serverIssues = [], onFix }) {
2869
+ const clientGroups = groupIssues(model, clientIssues);
2870
+ const serverGroups = groupIssues(model, serverIssues);
2871
+ const totalIssues = clientIssues.length + serverIssues.length;
2872
+ return /* @__PURE__ */ jsxs11("details", { className: "wf-validation-panel", open: totalIssues > 0, children: [
2873
+ /* @__PURE__ */ jsxs11("summary", { className: "wf-validation-panel__summary", children: [
2874
+ /* @__PURE__ */ jsx11("span", { className: "wf-validation-panel__summary-mobile", children: totalIssues === 0 ? ACTION_LABELS.workflowLooksGood : ACTION_LABELS.thingsToFix(totalIssues) }),
2875
+ /* @__PURE__ */ jsx11("span", { className: "wf-validation-panel__summary-desktop", children: ACTION_LABELS.clientValidation }),
2876
+ /* @__PURE__ */ jsx11("span", { className: "wf-validation-panel__chevron", "aria-hidden": true, children: "\u25BE" })
2877
+ ] }),
2878
+ /* @__PURE__ */ jsx11("div", { className: "wf-validation-panel__body", children: totalIssues === 0 ? /* @__PURE__ */ jsx11("p", { className: "wf-validation-panel__ok", children: ACTION_LABELS.checkmarkOk }) : /* @__PURE__ */ jsxs11(Fragment4, { children: [
2879
+ /* @__PURE__ */ jsx11("p", { className: "wf-validation-panel__count", children: ACTION_LABELS.thingsToFix(totalIssues) }),
2880
+ /* @__PURE__ */ jsx11(Section2, { title: ACTION_LABELS.clientValidation, groups: clientGroups, onFix }),
2881
+ /* @__PURE__ */ jsx11(Section2, { title: ACTION_LABELS.serverValidation, groups: serverGroups, onFix })
2882
+ ] }) })
2883
+ ] });
2884
+ }
2885
+
2886
+ // src/validation-ui/ValidationPill.tsx
2887
+ import { useState as useState5 } from "react";
2888
+ import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
2889
+ function ValidationPill({
2890
+ model,
2891
+ clientIssues,
2892
+ serverIssues = [],
2893
+ onFix,
2894
+ className
2895
+ }) {
2896
+ const [open, setOpen] = useState5(false);
2897
+ const totalIssues = clientIssues.length + serverIssues.length;
2898
+ const looksGood = totalIssues === 0;
2899
+ return /* @__PURE__ */ jsxs12(Fragment5, { children: [
2900
+ /* @__PURE__ */ jsxs12(
2901
+ "button",
2902
+ {
2903
+ type: "button",
2904
+ className: ["wf-button wf-button--secondary wf-validation-pill", className].filter(Boolean).join(" "),
2905
+ onClick: () => setOpen(true),
2906
+ "aria-label": looksGood ? ACTION_LABELS.looksGood : ACTION_LABELS.thingsToFix(totalIssues),
2907
+ children: [
2908
+ /* @__PURE__ */ jsx12("span", { className: "wf-validation-pill__glyph", "aria-hidden": true, children: looksGood ? "\u2713" : "!" }),
2909
+ /* @__PURE__ */ jsx12("span", { className: "wf-validation-pill__label-desktop", children: looksGood ? `\u2713 ${ACTION_LABELS.looksGood}` : ACTION_LABELS.thingsToFix(totalIssues) }),
2910
+ /* @__PURE__ */ jsx12("span", { className: "wf-validation-pill__label-mobile", children: looksGood ? ACTION_LABELS.okShort : String(totalIssues) })
2911
+ ]
2912
+ }
2913
+ ),
2914
+ open ? /* @__PURE__ */ jsx12("div", { className: "wf-validation-pill__overlay", role: "presentation", onClick: () => setOpen(false), children: /* @__PURE__ */ jsxs12(
2915
+ "div",
2916
+ {
2917
+ className: "wf-panel wf-validation-pill__drawer",
2918
+ role: "dialog",
2919
+ "aria-label": ACTION_LABELS.clientValidation,
2920
+ onClick: (e) => e.stopPropagation(),
2921
+ children: [
2922
+ /* @__PURE__ */ jsxs12("header", { className: "wf-panel__header", children: [
2923
+ /* @__PURE__ */ jsx12("h2", { className: "wf-panel__title", children: ACTION_LABELS.clientValidation }),
2924
+ /* @__PURE__ */ jsx12("p", { className: "wf-panel__description", children: ACTION_LABELS.validationDetailsDescription }),
2925
+ /* @__PURE__ */ jsx12(
2926
+ "button",
2927
+ {
2928
+ type: "button",
2929
+ className: "wf-button wf-button--icon wf-button--ghost wf-panel__close",
2930
+ "aria-label": "Close",
2931
+ onClick: () => setOpen(false),
2932
+ children: "\xD7"
2933
+ }
2934
+ )
2935
+ ] }),
2936
+ /* @__PURE__ */ jsx12("div", { className: "wf-panel__body", children: /* @__PURE__ */ jsx12(
2937
+ ValidationPanel,
2938
+ {
2939
+ model,
2940
+ clientIssues,
2941
+ serverIssues,
2942
+ onFix: (stepId) => {
2943
+ onFix(stepId);
2944
+ setOpen(false);
2945
+ }
2946
+ }
2947
+ ) })
2948
+ ]
2949
+ }
2950
+ ) }) : null
2951
+ ] });
2952
+ }
2953
+
2954
+ // src/io/core.ts
2955
+ var STRIP_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2956
+ var WorkflowParseError = class extends Error {
2957
+ constructor(message) {
2958
+ super(message);
2959
+ this.name = "WorkflowParseError";
2960
+ }
2961
+ };
2962
+ function assertNoProtoKey(value, path = "root") {
2963
+ if (value === null || typeof value !== "object") {
2964
+ return;
2965
+ }
2966
+ if (Array.isArray(value)) {
2967
+ value.forEach((item, index) => assertNoProtoKey(item, `${path}[${index}]`));
2968
+ return;
2969
+ }
2970
+ for (const key of Object.keys(value)) {
2971
+ if (key === "__proto__") {
2972
+ throw new WorkflowParseError(`Forbidden key "__proto__" at ${path}`);
2973
+ }
2974
+ assertNoProtoKey(value[key], `${path}.${key}`);
2975
+ }
2976
+ }
2977
+ function stripDangerousKeys(value) {
2978
+ if (value === null || typeof value !== "object") {
2979
+ return value;
2980
+ }
2981
+ if (Array.isArray(value)) {
2982
+ return value.map(stripDangerousKeys);
2983
+ }
2984
+ const result = {};
2985
+ for (const [key, child] of Object.entries(value)) {
2986
+ if (STRIP_KEYS.has(key)) {
2987
+ continue;
2988
+ }
2989
+ result[key] = stripDangerousKeys(child);
2990
+ }
2991
+ return result;
2992
+ }
2993
+ function serializeWorkflowJson(workflow) {
2994
+ return JSON.stringify(workflow, null, 2);
2995
+ }
2996
+ function parseWorkflowJson(text) {
2997
+ let parsed;
2998
+ try {
2999
+ parsed = JSON.parse(text);
3000
+ } catch {
3001
+ throw new WorkflowParseError("Invalid JSON");
3002
+ }
3003
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
3004
+ throw new WorkflowParseError("Workflow JSON must be an object");
3005
+ }
3006
+ assertNoProtoKey(parsed);
3007
+ return stripDangerousKeys(parsed);
3008
+ }
3009
+
3010
+ // src/io/browser/download.ts
3011
+ function downloadWorkflowJson(draft, filename = "workflow.json") {
3012
+ const blob = new Blob([serializeWorkflowJson(draft)], {
3013
+ type: "application/json"
3014
+ });
3015
+ const url = URL.createObjectURL(blob);
3016
+ const anchor = document.createElement("a");
3017
+ anchor.href = url;
3018
+ anchor.download = filename;
3019
+ anchor.click();
3020
+ URL.revokeObjectURL(url);
3021
+ }
3022
+ async function exportWorkflowBundle(draft, format) {
3023
+ if (format !== "json") {
3024
+ throw new Error(`Unsupported export format: ${format}`);
3025
+ }
3026
+ const name = typeof draft.name === "string" && draft.name.length > 0 ? `${draft.name}.json` : "workflow.json";
3027
+ downloadWorkflowJson(draft, name);
3028
+ }
3029
+
3030
+ // src/io/browser/upload.ts
3031
+ function readWorkflowJsonFile(file) {
3032
+ return file.text().then(parseWorkflowJson);
3033
+ }
3034
+ async function importWorkflowFromFilePicker() {
3035
+ return new Promise((resolve, reject) => {
3036
+ const input = document.createElement("input");
3037
+ input.type = "file";
3038
+ input.accept = "application/json,.json";
3039
+ input.onchange = () => {
3040
+ const file = input.files?.[0];
3041
+ if (!file) {
3042
+ reject(new Error("No file selected"));
3043
+ return;
3044
+ }
3045
+ readWorkflowJsonFile(file).then(resolve).catch(reject);
3046
+ };
3047
+ input.click();
3048
+ });
3049
+ }
3050
+
3051
+ // src/api/WorkflowBuilder.tsx
3052
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
3053
+ function flattenClientIssues(model, validation) {
3054
+ const safe = (value) => value ?? "";
3055
+ return [
3056
+ ...Object.entries(validation.workflow).map(([field, message]) => ({
3057
+ code: `workflow.${field}`,
3058
+ message: safe(message),
3059
+ stepId: void 0
3060
+ })),
3061
+ ...Object.entries(validation.steps).flatMap(
3062
+ ([idx, errs]) => Object.entries(errs).map(([field, message]) => ({
3063
+ code: `step.${field}`,
3064
+ message: safe(message),
3065
+ stepId: model.nodes[Number(idx)]?.backendStepId
3066
+ }))
3067
+ ),
3068
+ ...Object.entries(validation.artifacts).flatMap(
3069
+ ([artifactId, errs]) => Object.entries(errs).map(([field, message]) => ({
3070
+ code: `artifact.${artifactId}.${field}`,
3071
+ message: safe(message),
3072
+ stepId: void 0
3073
+ }))
3074
+ ),
3075
+ ...validation.global.map((message) => ({ code: "global", message: safe(message), stepId: void 0 }))
3076
+ ];
3077
+ }
3078
+ function hasApprovalTransition(node) {
3079
+ const data = node.data;
3080
+ return data.transition === "HUMAN_APPROVAL";
3081
+ }
3082
+ function hasConfiguredInput(node) {
3083
+ if (node.kind !== "ASK_USER") {
3084
+ return false;
3085
+ }
3086
+ return Boolean(node.data.name?.trim() || node.data.question?.trim());
3087
+ }
3088
+ function nextLibraryStepPosition(model, selectedId) {
3089
+ const GAP_X = 280;
3090
+ const GAP_Y = 140;
3091
+ const byId = new Map(model.nodes.map((node) => [node.id, node]));
3092
+ const outgoing2 = /* @__PURE__ */ new Map();
3093
+ const incomingCount = /* @__PURE__ */ new Map();
3094
+ for (const node of model.nodes) {
3095
+ outgoing2.set(node.id, []);
3096
+ incomingCount.set(node.id, 0);
3097
+ }
3098
+ for (const edge of model.edges) {
3099
+ outgoing2.set(edge.source, [...outgoing2.get(edge.source) ?? [], edge.target]);
3100
+ incomingCount.set(edge.target, (incomingCount.get(edge.target) ?? 0) + 1);
3101
+ }
3102
+ const selectedNode = selectedId ? byId.get(selectedId) ?? null : null;
3103
+ if (selectedNode) {
3104
+ return {
3105
+ position: { x: selectedNode.position.x + GAP_X, y: selectedNode.position.y },
3106
+ afterNodeId: selectedNode.id
3107
+ };
3108
+ }
3109
+ const start = model.startNodeId && byId.has(model.startNodeId) ? model.startNodeId : model.nodes[0]?.id ?? null;
3110
+ if (!start) {
3111
+ return { position: { x: 80, y: 120 }, afterNodeId: null };
3112
+ }
3113
+ let cursor = start;
3114
+ const visited = /* @__PURE__ */ new Set([cursor]);
3115
+ while (true) {
3116
+ const next = (outgoing2.get(cursor) ?? []).find((target) => (incomingCount.get(target) ?? 0) <= 1);
3117
+ if (!next || visited.has(next)) {
3118
+ break;
3119
+ }
3120
+ visited.add(next);
3121
+ cursor = next;
3122
+ }
3123
+ const anchor = byId.get(cursor);
3124
+ if (!anchor) {
3125
+ return { position: { x: 80, y: 120 }, afterNodeId: null };
3126
+ }
3127
+ return {
3128
+ position: {
3129
+ x: anchor.position.x + GAP_X,
3130
+ y: anchor.position.y + (anchor.kind === "DECISION" ? GAP_Y : 0)
3131
+ },
3132
+ afterNodeId: anchor.id
3133
+ };
3134
+ }
3135
+ function WorkflowBuilder({
3136
+ capabilities,
3137
+ adapters,
3138
+ actions,
3139
+ theme,
3140
+ initialWorkflow,
3141
+ agentCatalog = []
3142
+ }) {
3143
+ const seed = initialWorkflow ?? emptyWorkflow();
3144
+ const { state, dispatch, dirty } = useBuilderState(seed);
3145
+ const [pending, setPending] = useState6({});
3146
+ const [errors, setErrors] = useState6({});
3147
+ const skipDraftSync = useRef(false);
3148
+ const initialCanvas = useMemo4(
3149
+ () => seed.steps.length > 0 ? workflowToCanvas(seed) : createInitialCanvasModel(),
3150
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- canvas seed is mount-only
3151
+ []
3152
+ );
3153
+ const {
3154
+ model,
3155
+ setModel,
3156
+ setModelFromLoad,
3157
+ selectedId,
3158
+ setSelectedId,
3159
+ markClean,
3160
+ updateNodeData,
3161
+ appendNode
3162
+ } = useCanvasState(initialCanvas);
3163
+ const { mode, setMode } = useBuilderMode(model, !initialWorkflow?.id);
3164
+ const { buildFromCanvas } = useWorkflowDraft();
3165
+ const resolvedAdapters = useMemo4(
3166
+ () => ({
3167
+ validateWorkflow: adapters?.validateWorkflow ?? validateWorkflow,
3168
+ importBundle: adapters?.importBundle ?? importWorkflowFromFilePicker,
3169
+ exportBundle: adapters?.exportBundle ?? exportWorkflowBundle
3170
+ }),
3171
+ [adapters]
3172
+ );
3173
+ useEffect3(() => {
3174
+ if (skipDraftSync.current) {
3175
+ skipDraftSync.current = false;
3176
+ return;
3177
+ }
3178
+ const draft = canvasToWorkflow(model);
3179
+ dispatch({ type: "SET_DRAFT", draft });
3180
+ }, [model, dispatch]);
3181
+ useEffect3(() => {
3182
+ let cancelled = false;
3183
+ void Promise.resolve(resolvedAdapters.validateWorkflow(state.draft)).then((result) => {
3184
+ if (!cancelled) {
3185
+ dispatch({ type: "SET_VALIDATION", validation: result });
3186
+ }
3187
+ });
3188
+ return () => {
3189
+ cancelled = true;
3190
+ };
3191
+ }, [state.draft, resolvedAdapters, dispatch]);
3192
+ const clientValidation = useMemo4(() => buildFromCanvas(model).validation, [buildFromCanvas, model]);
3193
+ const clientIssues = useMemo4(() => flattenClientIssues(model, clientValidation), [clientValidation, model]);
3194
+ const issueCountByBackendStepId = useMemo4(() => {
3195
+ const counts = {};
3196
+ for (const issue of clientIssues) {
3197
+ if (!issue.stepId) {
3198
+ continue;
3199
+ }
3200
+ counts[issue.stepId] = (counts[issue.stepId] ?? 0) + 1;
3201
+ }
3202
+ return counts;
3203
+ }, [clientIssues]);
3204
+ const runAction = useCallback5(async (key, fn) => {
3205
+ setPending((prev) => ({ ...prev, [key]: true }));
3206
+ setErrors((prev) => ({ ...prev, [key]: null }));
3207
+ try {
3208
+ await fn();
3209
+ } catch (err) {
3210
+ const message = err instanceof Error ? err.message : "Action failed";
3211
+ setErrors((prev) => ({ ...prev, [key]: message }));
3212
+ } finally {
3213
+ setPending((prev) => ({ ...prev, [key]: false }));
3214
+ }
3215
+ }, []);
3216
+ const handleImport = () => runAction("import", async () => {
3217
+ const imported = await resolvedAdapters.importBundle();
3218
+ skipDraftSync.current = true;
3219
+ setModelFromLoad(workflowToCanvas(imported));
3220
+ dispatch({ type: "SET_DRAFT", draft: imported });
3221
+ dispatch({ type: "SET_BASELINE", baseline: imported });
3222
+ dispatch({
3223
+ type: "SET_IMPORT_META",
3224
+ importMeta: { source: "file", importedAt: (/* @__PURE__ */ new Date()).toISOString() }
3225
+ });
3226
+ });
3227
+ const handleExport = () => runAction("export", async () => {
3228
+ await resolvedAdapters.exportBundle(state.draft, "json");
3229
+ });
3230
+ const handleSave = () => runAction("save", async () => {
3231
+ if (!actions?.save) {
3232
+ throw new Error("Save action is not configured");
3233
+ }
3234
+ await actions.save(state.draft);
3235
+ dispatch({ type: "SET_BASELINE", baseline: state.draft });
3236
+ markClean();
3237
+ });
3238
+ const handleRun = () => runAction("run", async () => {
3239
+ if (!actions?.run) {
3240
+ throw new Error("Run action is not configured");
3241
+ }
3242
+ await actions.run(state.draft);
3243
+ });
3244
+ const handlePublish = () => runAction("publish", async () => {
3245
+ if (!actions?.publish) {
3246
+ throw new Error("Publish action is not configured");
3247
+ }
3248
+ await actions.publish(state.draft);
3249
+ });
3250
+ const onAddStepFromLibrary = useCallback5(
3251
+ (kind, options) => {
3252
+ const { position, afterNodeId } = nextLibraryStepPosition(model, selectedId);
3253
+ const prefix = {
3254
+ ASK_USER: "ask-user",
3255
+ AI_STEP: "ai-step",
3256
+ AI_DEBATE: "ai-debate",
3257
+ DECISION: "decision",
3258
+ REPEAT: "repeat",
3259
+ SAVE_RESULT: "save-result",
3260
+ REUSE_WORKFLOW: "reuse-wf",
3261
+ LOAD_RESOURCE: "load-res",
3262
+ STOP: "stop",
3263
+ RETRY: "retry"
3264
+ };
3265
+ const backendStepId = newStepId(prefix[kind]);
3266
+ const id = `c-${backendStepId}`;
3267
+ const node = {
3268
+ id,
3269
+ backendStepId,
3270
+ kind,
3271
+ position,
3272
+ data: { ...defaultNodeData(kind), ...options?.patch }
3273
+ };
3274
+ setModel((m) => {
3275
+ const edge = afterNodeId && !m.edges.some((e) => e.source === afterNodeId && e.target === id) ? [
3276
+ {
3277
+ id: `e-${afterNodeId}-${id}-auto`,
3278
+ source: afterNodeId,
3279
+ target: id,
3280
+ sourceHandle: null,
3281
+ label: null
3282
+ }
3283
+ ] : [];
3284
+ return {
3285
+ ...m,
3286
+ nodes: [...m.nodes, node],
3287
+ edges: [...m.edges, ...edge],
3288
+ startNodeId: m.startNodeId ?? id
3289
+ };
3290
+ });
3291
+ setSelectedId(id);
3292
+ },
3293
+ [model, selectedId, setModel, setSelectedId]
3294
+ );
3295
+ const focusIssue = useCallback5(
3296
+ (stepId) => {
3297
+ if (!stepId) {
3298
+ return;
3299
+ }
3300
+ const node = model.nodes.find((n) => n.backendStepId === stepId);
3301
+ if (node) {
3302
+ setSelectedId(node.id);
3303
+ }
3304
+ },
3305
+ [model.nodes, setSelectedId]
3306
+ );
3307
+ const askUserNode = model.nodes.find((n) => n.kind === "ASK_USER");
3308
+ const guidedStages = useMemo4(
3309
+ () => [
3310
+ {
3311
+ label: GUIDED_STAGE_LABELS.addInput,
3312
+ complete: model.nodes.some((node) => node.kind === "ASK_USER" && hasConfiguredInput(node)),
3313
+ actionLabel: GUIDED_STAGE_LABELS.configureInput
3314
+ },
3315
+ {
3316
+ label: GUIDED_STAGE_LABELS.addAiStep,
3317
+ complete: model.nodes.some((node) => node.kind === "AI_STEP" || node.kind === "AI_DEBATE"),
3318
+ actionLabel: GUIDED_STAGE_LABELS.addAiStep
3319
+ },
3320
+ {
3321
+ label: GUIDED_STAGE_LABELS.addApproval,
3322
+ complete: model.nodes.some((node) => hasApprovalTransition(node)),
3323
+ actionLabel: GUIDED_STAGE_LABELS.requireApproval
3324
+ },
3325
+ {
3326
+ label: GUIDED_STAGE_LABELS.generateResult,
3327
+ complete: model.nodes.some((node) => node.kind === "SAVE_RESULT" || node.kind === "STOP"),
3328
+ actionLabel: GUIDED_STAGE_LABELS.addSaveStep
3329
+ }
3330
+ ],
3331
+ [model.nodes]
3332
+ );
3333
+ const activeGuidedIndex = guidedStages.findIndex((stage) => !stage.complete);
3334
+ const onGuidedStageAction = useCallback5(
3335
+ (index) => {
3336
+ switch (index) {
3337
+ case 0:
3338
+ if (askUserNode) {
3339
+ setSelectedId(askUserNode.id);
3340
+ }
3341
+ break;
3342
+ case 1:
3343
+ onAddStepFromLibrary("AI_STEP");
3344
+ break;
3345
+ case 2: {
3346
+ const aiNode = model.nodes.find((n) => n.kind === "AI_STEP");
3347
+ if (aiNode) {
3348
+ updateNodeData(aiNode.id, { transition: "HUMAN_APPROVAL" });
3349
+ setSelectedId(aiNode.id);
3350
+ } else {
3351
+ onAddStepFromLibrary("AI_STEP", { patch: { transition: "HUMAN_APPROVAL" } });
3352
+ }
3353
+ break;
3354
+ }
3355
+ case 3:
3356
+ onAddStepFromLibrary("SAVE_RESULT");
3357
+ break;
3358
+ default:
3359
+ break;
3360
+ }
3361
+ },
3362
+ [askUserNode, model.nodes, onAddStepFromLibrary, setSelectedId, updateNodeData]
3363
+ );
3364
+ const rootStyle = theme?.variables ? { style: theme.variables } : void 0;
3365
+ const rootClass = ["workflow-builder", theme?.className].filter(Boolean).join(" ");
3366
+ const activeError = Object.values(errors).find((value) => value) ?? null;
3367
+ const subtitle2 = [
3368
+ dirty ? ACTION_LABELS.unsavedChanges : ACTION_LABELS.upToDate,
3369
+ state.validation.valid ? null : ACTION_LABELS.validationIssues
3370
+ ].filter(Boolean).join(" \xB7 ");
3371
+ return /* @__PURE__ */ jsxs13("div", { className: rootClass, "data-testid": "workflow-builder", ...rootStyle, children: [
3372
+ /* @__PURE__ */ jsxs13("header", { className: "workflow-builder__header", children: [
3373
+ /* @__PURE__ */ jsx13(
3374
+ "input",
3375
+ {
3376
+ className: "workflow-builder__name-input",
3377
+ value: model.workflowName,
3378
+ placeholder: ACTION_LABELS.workflowNamePlaceholder,
3379
+ "aria-label": ACTION_LABELS.workflowNameLabel,
3380
+ onChange: (e) => setModel((m) => ({ ...m, workflowName: e.target.value }))
3381
+ }
3382
+ ),
3383
+ /* @__PURE__ */ jsx13(
3384
+ "input",
3385
+ {
3386
+ className: "workflow-builder__id-input",
3387
+ value: model.workflowId,
3388
+ placeholder: ACTION_LABELS.workflowIdPlaceholder,
3389
+ "aria-label": ACTION_LABELS.workflowIdLabel,
3390
+ onChange: (e) => setModel((m) => ({ ...m, workflowId: e.target.value }))
3391
+ }
3392
+ ),
3393
+ /* @__PURE__ */ jsxs13("div", { className: "workflow-builder__mode-toggle", role: "group", "aria-label": "Builder mode", children: [
3394
+ /* @__PURE__ */ jsx13(
3395
+ "button",
3396
+ {
3397
+ type: "button",
3398
+ className: ["wf-button", mode === "guided" ? "wf-button--primary" : "wf-button--ghost"].join(" "),
3399
+ onClick: () => setMode("guided"),
3400
+ children: ACTION_LABELS.guidedMode
3401
+ }
3402
+ ),
3403
+ /* @__PURE__ */ jsx13(
3404
+ "button",
3405
+ {
3406
+ type: "button",
3407
+ className: ["wf-button", mode === "advanced" ? "wf-button--primary" : "wf-button--ghost"].join(" "),
3408
+ onClick: () => setMode("advanced"),
3409
+ children: ACTION_LABELS.advancedMode
3410
+ }
3411
+ )
3412
+ ] }),
3413
+ /* @__PURE__ */ jsx13(ValidationPill, { model, clientIssues, onFix: focusIssue }),
3414
+ capabilities.export ? /* @__PURE__ */ jsx13(
3415
+ "button",
3416
+ {
3417
+ type: "button",
3418
+ className: "wf-button wf-button--ghost",
3419
+ disabled: pending.export,
3420
+ "data-testid": "workflow-builder-export",
3421
+ onClick: () => void handleExport(),
3422
+ children: pending.export ? ACTION_LABELS.exporting : ACTION_LABELS.export
3423
+ }
3424
+ ) : null,
3425
+ capabilities.save ? /* @__PURE__ */ jsx13(
3426
+ "button",
3427
+ {
3428
+ type: "button",
3429
+ className: "wf-button wf-button--primary",
3430
+ disabled: pending.save,
3431
+ "data-testid": "workflow-builder-save",
3432
+ onClick: () => void handleSave(),
3433
+ children: pending.save ? ACTION_LABELS.saving : ACTION_LABELS.save
3434
+ }
3435
+ ) : null,
3436
+ capabilities.run ? /* @__PURE__ */ jsx13(
3437
+ "button",
3438
+ {
3439
+ type: "button",
3440
+ className: "wf-button wf-button--secondary",
3441
+ disabled: pending.run,
3442
+ "data-testid": "workflow-builder-run",
3443
+ onClick: () => void handleRun(),
3444
+ children: pending.run ? ACTION_LABELS.running : ACTION_LABELS.run
3445
+ }
3446
+ ) : null,
3447
+ capabilities.publish ? /* @__PURE__ */ jsx13(
3448
+ "button",
3449
+ {
3450
+ type: "button",
3451
+ className: "wf-button wf-button--primary",
3452
+ disabled: pending.publish,
3453
+ "data-testid": "workflow-builder-publish",
3454
+ onClick: () => void handlePublish(),
3455
+ children: pending.publish ? ACTION_LABELS.publishing : ACTION_LABELS.publish
3456
+ }
3457
+ ) : null,
3458
+ capabilities.aiAssist ? /* @__PURE__ */ jsx13(
3459
+ "button",
3460
+ {
3461
+ type: "button",
3462
+ className: "wf-button wf-button--secondary",
3463
+ "data-testid": "workflow-builder-ai",
3464
+ "aria-label": ACTION_LABELS.aiAssist,
3465
+ children: ACTION_LABELS.aiAssist
3466
+ }
3467
+ ) : null,
3468
+ /* @__PURE__ */ jsx13("p", { className: "workflow-builder__subtitle", children: subtitle2 })
3469
+ ] }),
3470
+ model.unsupported ? /* @__PURE__ */ jsxs13("div", { className: "workflow-builder__banner workflow-builder__banner--warning", role: "status", children: [
3471
+ /* @__PURE__ */ jsx13("p", { className: "workflow-builder__banner-title", children: ACTION_LABELS.unsupportedBannerTitle }),
3472
+ model.unsupportedReasons?.length ? /* @__PURE__ */ jsx13("ul", { className: "workflow-builder__banner-list", children: model.unsupportedReasons.map((reason, index) => /* @__PURE__ */ jsx13("li", { children: reason }, index)) }) : null
3473
+ ] }) : null,
3474
+ /* @__PURE__ */ jsxs13("div", { className: "workflow-builder__workspace", children: [
3475
+ mode === "guided" ? /* @__PURE__ */ jsx13("div", { className: "workflow-builder__guided", children: /* @__PURE__ */ jsx13(
3476
+ GuidedStepper,
3477
+ {
3478
+ stages: guidedStages,
3479
+ activeIndex: activeGuidedIndex === -1 ? guidedStages.length - 1 : activeGuidedIndex,
3480
+ onStageAction: onGuidedStageAction
3481
+ }
3482
+ ) }) : null,
3483
+ /* @__PURE__ */ jsx13(
3484
+ StepPalette,
3485
+ {
3486
+ mode,
3487
+ onAddStep: (kind) => onAddStepFromLibrary(kind),
3488
+ defaultCollapsed: mode !== "advanced"
3489
+ }
3490
+ ),
3491
+ /* @__PURE__ */ jsx13("div", { className: "workflow-builder__canvas", "data-testid": "workflow-builder-canvas", children: /* @__PURE__ */ jsx13(
3492
+ WorkflowCanvas,
3493
+ {
3494
+ model,
3495
+ onModelChange: setModel,
3496
+ onSelectNode: setSelectedId,
3497
+ selectedId,
3498
+ onAppend: appendNode,
3499
+ issueCountByBackendStepId
3500
+ }
3501
+ ) }),
3502
+ /* @__PURE__ */ jsx13(
3503
+ StepConfigPanel,
3504
+ {
3505
+ model,
3506
+ selectedId,
3507
+ mode,
3508
+ onClose: () => setSelectedId(null),
3509
+ onUpdateNodeData: updateNodeData,
3510
+ agentCatalog
3511
+ }
3512
+ )
3513
+ ] }),
3514
+ /* @__PURE__ */ jsx13("div", { className: "workflow-builder__actions", children: capabilities.import ? /* @__PURE__ */ jsx13(
3515
+ "button",
3516
+ {
3517
+ type: "button",
3518
+ className: "workflow-builder__button",
3519
+ "data-testid": "workflow-builder-import",
3520
+ disabled: pending.import,
3521
+ onClick: () => void handleImport(),
3522
+ children: pending.import ? ACTION_LABELS.importing : ACTION_LABELS.import
3523
+ }
3524
+ ) : null }),
3525
+ activeError ? /* @__PURE__ */ jsx13("p", { className: "workflow-builder__status workflow-builder__status--error", role: "alert", children: activeError }) : null
3526
+ ] });
3527
+ }
3528
+ export {
3529
+ WorkflowBuilder,
3530
+ WorkflowParseError,
3531
+ builderReducer,
3532
+ WorkflowBuilder as default,
3533
+ parseWorkflowJson,
3534
+ serializeWorkflowJson,
3535
+ useBuilderState,
3536
+ validateWorkflow
3537
+ };
3538
+ //# sourceMappingURL=index.js.map