@growthub/cli 0.12.2 → 0.13.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.
Files changed (31) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +50 -25
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +141 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryReviewModal.jsx +38 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +556 -248
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +242 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +52 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +1203 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +163 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxOrchestrationEditorPanel.jsx +190 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolConfirmModal.jsx +64 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolDraftPanel.jsx +376 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/dm-shared.jsx +8 -2
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +6 -1
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +2897 -934
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +10 -7
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/views/[viewId]/page.jsx +206 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +906 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/page.jsx +12 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +493 -28
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +1363 -8
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/nav-workflows.js +54 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +322 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +734 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +73 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-sidecar-routing.js +24 -0
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +13 -4
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper-apply.js +96 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +122 -0
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +1 -0
  31. package/package.json +1 -1
@@ -0,0 +1,1203 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import {
5
+ detectFieldIdsFromLastResponse,
6
+ FILTER_CONJUNCTIONS,
7
+ FILTER_OPERATORS,
8
+ isApiRegistryTestSuccessful
9
+ } from "@/lib/orchestration-graph";
10
+
11
+ const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
12
+ const MODEL_OPTIONS = ["Claude Opus 4.6", "Claude Sonnet 4.5", "GPT-5.2", "Local agent host"];
13
+ const OUTPUT_TYPES = ["Text", "Number", "Boolean", "JSON", "Record ID"];
14
+ function normalizeTags(tags) {
15
+ return Array.from(new Set((Array.isArray(tags) ? tags : [])
16
+ .map((tag) => String(tag || "").trim().toLowerCase())
17
+ .filter(Boolean)));
18
+ }
19
+
20
+ function inferDeltaTagsForNode(node, config) {
21
+ const tags = [];
22
+ const type = String(node?.type || "").trim();
23
+ const action = String(config?.action || node?.id || "").trim();
24
+
25
+ if (type === "thinAdapter") tags.push("model", "prompt", "routing");
26
+ if (type === "ai-agent") tags.push("model", "prompt", "output");
27
+ if (type === "data-action" || type === "data-trigger") tags.push("input", "output");
28
+ if (type === "flow-control") tags.push("routing");
29
+ if (type === "core-action") tags.push("runtime");
30
+ if (type === "human-input") tags.push("input");
31
+
32
+ if (action.includes("search") || action.includes("filter") || type === "transform-filter") tags.push("evaluation", "guardrail");
33
+ if (action.includes("delete") || config?.confirmationRequired) tags.push("guardrail");
34
+ if (action.includes("http") || config?.url || config?.method) tags.push("routing", "input", "output");
35
+ if (action.includes("email")) tags.push("input", "output");
36
+ if (action.includes("delay") || config?.duration || config?.unit) tags.push("runtime");
37
+ if (config?.objectId || config?.fieldMap || config?.filters) tags.push("input", "output");
38
+ if (config?.model || config?.prompt) tags.push("model", "prompt");
39
+
40
+ return normalizeTags(tags);
41
+ }
42
+
43
+ function getDeltaTagDefaultValue(tag, node, config, sandboxRow) {
44
+ const normalized = String(tag || "").trim().toLowerCase();
45
+ if (normalized === "model") {
46
+ return String(config?.model || sandboxRow?.adapter || sandboxRow?.executionAdapter || sandboxRow?.runAdapter || node?.sandbox || "").trim();
47
+ }
48
+ if (normalized === "prompt") {
49
+ return String(config?.prompt || sandboxRow?.prompt || sandboxRow?.instructions || sandboxRow?.command || "").trim();
50
+ }
51
+ if (normalized === "routing") {
52
+ return String(config?.inputBinding || config?.executionPolicy || config?.filterMode || config?.endpoint || config?.url || "").trim();
53
+ }
54
+ if (normalized === "input") {
55
+ return String(config?.objectName || config?.objectId || config?.inputBinding || config?.bodyTemplate || config?.samplePayload ? (
56
+ config?.objectName || config?.objectId || config?.inputBinding || "configured input"
57
+ ) : "").trim();
58
+ }
59
+ if (normalized === "output") {
60
+ return String(config?.outputKey || config?.outputVariable || config?.outputField || config?.fieldMap ? (
61
+ config?.outputKey || config?.outputVariable || config?.outputField || "configured output"
62
+ ) : "").trim();
63
+ }
64
+ if (normalized === "evaluation") {
65
+ return String(config?.filters || config?.successStatusCodes || config?.expectedStatus ? "configured evaluation" : "").trim();
66
+ }
67
+ if (normalized === "guardrail") {
68
+ return String(config?.confirmationRequired ? "confirmation required" : config?.networkPolicy || sandboxRow?.networkPolicy || "").trim();
69
+ }
70
+ if (normalized === "runtime") {
71
+ return String(config?.duration || config?.unit ? `${config.duration || ""} ${config.unit || ""}`.trim() : sandboxRow?.runtime || "").trim();
72
+ }
73
+ return String(config?.deltaValues?.[normalized] || "").trim();
74
+ }
75
+
76
+ function getObjectFields(object) {
77
+ return (Array.isArray(object?.fields) ? object.fields : [])
78
+ .map((field) => ({
79
+ id: String(field.id || field.name || field.label || "").trim(),
80
+ label: String(field.label || field.name || field.id || "").trim(),
81
+ type: String(field.type || field.fieldType || "text").trim()
82
+ }))
83
+ .filter((field) => field.id);
84
+ }
85
+
86
+ function getSelectedObject(workspaceObjects, objectId) {
87
+ return workspaceObjects.find((object) => String(object.id) === String(objectId || "")) || null;
88
+ }
89
+
90
+ function KeyValueRows({ label, entries, onChange, disabled, keyPlaceholder = "Key", valuePlaceholder = "Value" }) {
91
+ const rows = Array.isArray(entries)
92
+ ? entries
93
+ : Object.entries(entries && typeof entries === "object" ? entries : {}).map(([key, value]) => ({ key, value }));
94
+
95
+ function normalize(nextRows) {
96
+ onChange(nextRows.filter((row) => String(row.key || row.name || "").trim()));
97
+ }
98
+
99
+ function updateRow(index, patch) {
100
+ const next = rows.map((row, i) => (i === index ? { ...row, ...patch } : row));
101
+ normalize(next);
102
+ }
103
+
104
+ return (
105
+ <div className="dm-orchestration-config__fieldmap">
106
+ <span className="dm-orchestration-config__field-label">{label}</span>
107
+ {rows.map((row, index) => (
108
+ <div key={index} className="dm-orchestration-config__fieldmap-row">
109
+ <input
110
+ placeholder={keyPlaceholder}
111
+ value={row.key || row.name || ""}
112
+ disabled={disabled}
113
+ onChange={(e) => updateRow(index, { key: e.target.value })}
114
+ />
115
+ <input
116
+ placeholder={valuePlaceholder}
117
+ value={row.value == null ? "" : String(row.value)}
118
+ disabled={disabled}
119
+ onChange={(e) => updateRow(index, { value: e.target.value })}
120
+ />
121
+ <button type="button" className="dm-btn-ghost" disabled={disabled} onClick={() => normalize(rows.filter((_, i) => i !== index))}>
122
+ Remove
123
+ </button>
124
+ </div>
125
+ ))}
126
+ <button type="button" className="dm-btn-outline" disabled={disabled} onClick={() => normalize([...rows, { key: "", value: "" }])}>
127
+ + Add {label.toLowerCase()}
128
+ </button>
129
+ </div>
130
+ );
131
+ }
132
+
133
+ function FilterClauseList({ filters, filterMode, onChange, disabled, fieldOptions = [] }) {
134
+ const clauses = Array.isArray(filters) ? filters : [];
135
+ const mode = FILTER_CONJUNCTIONS.includes(filterMode) ? filterMode : "and";
136
+
137
+ function updateClause(index, patch) {
138
+ const next = clauses.map((c, i) => (i === index ? { ...c, ...patch } : c));
139
+ onChange(next, mode);
140
+ }
141
+
142
+ function addClause() {
143
+ onChange([...clauses, { fieldId: "", operator: "eq", value: "" }], mode);
144
+ }
145
+
146
+ function removeClause(index) {
147
+ onChange(clauses.filter((_, i) => i !== index), mode);
148
+ }
149
+
150
+ return (
151
+ <div className="dm-orchestration-config__filters">
152
+ <p className="dm-orchestration-config__hint">Where</p>
153
+ <label className="dm-orchestration-config__field">
154
+ <span>Match</span>
155
+ <select value={mode} disabled={disabled} onChange={(e) => onChange(clauses, e.target.value)}>
156
+ {FILTER_CONJUNCTIONS.map((c) => (
157
+ <option key={c} value={c}>{c}</option>
158
+ ))}
159
+ </select>
160
+ </label>
161
+ {clauses.map((clause, index) => (
162
+ <div key={index} className="dm-orchestration-config__filter-row">
163
+ {fieldOptions.length > 0 ? (
164
+ <select
165
+ value={clause.fieldId || ""}
166
+ disabled={disabled}
167
+ onChange={(e) => updateClause(index, { fieldId: e.target.value })}
168
+ >
169
+ <option value="">Select field</option>
170
+ {fieldOptions.map((field) => (
171
+ <option key={field} value={field}>{field}</option>
172
+ ))}
173
+ </select>
174
+ ) : (
175
+ <input
176
+ placeholder="field"
177
+ value={clause.fieldId || ""}
178
+ disabled={disabled}
179
+ onChange={(e) => updateClause(index, { fieldId: e.target.value })}
180
+ />
181
+ )}
182
+ <select
183
+ value={clause.operator || "eq"}
184
+ disabled={disabled}
185
+ onChange={(e) => updateClause(index, { operator: e.target.value })}
186
+ >
187
+ {FILTER_OPERATORS.map((op) => (
188
+ <option key={op} value={op}>{op}</option>
189
+ ))}
190
+ </select>
191
+ <input
192
+ placeholder="value"
193
+ value={clause.value ?? ""}
194
+ disabled={disabled || clause.operator === "isEmpty" || clause.operator === "isNotEmpty"}
195
+ onChange={(e) => updateClause(index, { value: e.target.value })}
196
+ />
197
+ <button type="button" className="dm-btn-ghost" disabled={disabled} onClick={() => removeClause(index)}>
198
+ Remove
199
+ </button>
200
+ </div>
201
+ ))}
202
+ <button type="button" className="dm-btn-outline dm-orchestration-config__add-filter" disabled={disabled} onClick={addClause}>
203
+ + Add filter rule
204
+ </button>
205
+ </div>
206
+ );
207
+ }
208
+
209
+ function FieldMapRows({ fieldMap, onChange, disabled, fieldOptions = [] }) {
210
+ const entries = Object.entries(fieldMap && typeof fieldMap === "object" ? fieldMap : {});
211
+
212
+ function updateEntry(index, target, sourcePath) {
213
+ const next = [...entries];
214
+ next[index] = [target, sourcePath];
215
+ onChange(Object.fromEntries(next.filter(([t]) => String(t).trim())));
216
+ }
217
+
218
+ function addEntry() {
219
+ onChange({ ...Object.fromEntries(entries), "": "" });
220
+ }
221
+
222
+ function removeEntry(index) {
223
+ const next = entries.filter((_, i) => i !== index);
224
+ onChange(Object.fromEntries(next));
225
+ }
226
+
227
+ return (
228
+ <div className="dm-orchestration-config__fieldmap">
229
+ <span className="dm-orchestration-config__field-label">Field mapping</span>
230
+ {entries.length === 0 && (
231
+ <p className="dm-orchestration-config__hint">Map workspace field names to paths in the API response.</p>
232
+ )}
233
+ {entries.map(([target, sourcePath], index) => (
234
+ <div key={`${target}-${index}`} className="dm-orchestration-config__fieldmap-row">
235
+ <input
236
+ placeholder="Output field"
237
+ value={target}
238
+ disabled={disabled}
239
+ onChange={(e) => updateEntry(index, e.target.value, sourcePath)}
240
+ />
241
+ {fieldOptions.length > 0 ? (
242
+ <select
243
+ value={sourcePath || ""}
244
+ disabled={disabled}
245
+ onChange={(e) => updateEntry(index, target, e.target.value)}
246
+ >
247
+ <option value="">Source path</option>
248
+ {fieldOptions.map((field) => (
249
+ <option key={field} value={field}>{field}</option>
250
+ ))}
251
+ </select>
252
+ ) : (
253
+ <input
254
+ placeholder="Source path"
255
+ value={sourcePath || ""}
256
+ disabled={disabled}
257
+ onChange={(e) => updateEntry(index, target, e.target.value)}
258
+ />
259
+ )}
260
+ <button type="button" className="dm-btn-ghost" disabled={disabled} onClick={() => removeEntry(index)}>
261
+ Remove
262
+ </button>
263
+ </div>
264
+ ))}
265
+ <button type="button" className="dm-btn-outline" disabled={disabled} onClick={addEntry}>
266
+ + Add field mapping
267
+ </button>
268
+ </div>
269
+ );
270
+ }
271
+
272
+ function PayloadKeyRows({ payload, onChange, disabled }) {
273
+ const entries = Object.entries(payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {});
274
+
275
+ function setEntries(nextEntries) {
276
+ onChange(Object.fromEntries(nextEntries.filter(([k]) => String(k).trim())));
277
+ }
278
+
279
+ return (
280
+ <div className="dm-orchestration-config__payload">
281
+ <span className="dm-orchestration-config__field-label">Test payload fields</span>
282
+ {entries.map(([key, value], index) => (
283
+ <div key={index} className="dm-orchestration-config__payload-row">
284
+ <input
285
+ placeholder="key"
286
+ value={key}
287
+ disabled={disabled}
288
+ onChange={(e) => {
289
+ const next = [...entries];
290
+ next[index] = [e.target.value, value];
291
+ setEntries(next);
292
+ }}
293
+ />
294
+ <input
295
+ placeholder="value"
296
+ value={value == null ? "" : String(value)}
297
+ disabled={disabled}
298
+ onChange={(e) => {
299
+ const next = [...entries];
300
+ next[index] = [key, e.target.value];
301
+ setEntries(next);
302
+ }}
303
+ />
304
+ <button
305
+ type="button"
306
+ className="dm-btn-ghost"
307
+ disabled={disabled}
308
+ onClick={() => setEntries(entries.filter((_, i) => i !== index))}
309
+ >
310
+ Remove
311
+ </button>
312
+ </div>
313
+ ))}
314
+ <button
315
+ type="button"
316
+ className="dm-btn-outline"
317
+ disabled={disabled}
318
+ onClick={() => setEntries([...entries, ["", ""]])}
319
+ >
320
+ + Add payload field
321
+ </button>
322
+ </div>
323
+ );
324
+ }
325
+
326
+ function VersionDeltaControls({ node, config, sandboxRow, onChange, disabled }) {
327
+ const explicitTags = normalizeTags(config?.deltaTags);
328
+ const inferredTags = inferDeltaTagsForNode(node, config);
329
+ const selectedTags = explicitTags.length > 0 ? explicitTags : inferredTags;
330
+ const deltaValues = config?.deltaValues && typeof config.deltaValues === "object" && !Array.isArray(config.deltaValues)
331
+ ? config.deltaValues
332
+ : {};
333
+
334
+ function patch(patchValue) {
335
+ onChange?.({ ...(config || {}), ...patchValue });
336
+ }
337
+
338
+ function patchDeltaValue(tag, value) {
339
+ patch({ deltaValues: { ...deltaValues, [tag]: value } });
340
+ }
341
+
342
+ return (
343
+ <div className="dm-orchestration-config__section dm-version-delta">
344
+ <label className="dm-orchestration-config__field">
345
+ <span className="dm-version-delta__label-row">
346
+ Delta tags
347
+ <span
348
+ className="dm-version-delta__info"
349
+ title={explicitTags.length > 0 ? "Saved orchestration config tags." : "Derived from this node's real bindings."}
350
+ aria-label={explicitTags.length > 0 ? "Saved orchestration config tags" : "Derived from this node's real bindings"}
351
+ >
352
+ i
353
+ </span>
354
+ </span>
355
+ <input
356
+ value={selectedTags.join(", ")}
357
+ placeholder="routing, prompt, evaluation"
358
+ disabled={disabled}
359
+ onChange={(event) => patch({ deltaTags: normalizeTags(event.target.value.split(",")) })}
360
+ />
361
+ </label>
362
+ {selectedTags.length > 0 && (
363
+ <div className="dm-version-delta__tag-fields">
364
+ {selectedTags.map((tag) => (
365
+ <label key={tag} className="dm-orchestration-config__field">
366
+ <span>{tag} value</span>
367
+ <input
368
+ value={deltaValues[tag] ?? getDeltaTagDefaultValue(tag, node, config, sandboxRow)}
369
+ placeholder={`Set ${tag} delta value`}
370
+ disabled={disabled}
371
+ onChange={(event) => patchDeltaValue(tag, event.target.value)}
372
+ />
373
+ </label>
374
+ ))}
375
+ </div>
376
+ )}
377
+ </div>
378
+ );
379
+ }
380
+
381
+ export function OrchestrationNodeConfigPanel({
382
+ node,
383
+ onConfigChange,
384
+ onDeleteNode,
385
+ disabled,
386
+ registryRow,
387
+ workspaceConfig,
388
+ sandboxRow,
389
+ activeTab: controlledTab,
390
+ onTabChange
391
+ }) {
392
+ const [internalTab, setInternalTab] = useState("node");
393
+ const rawActiveTab = controlledTab ?? internalTab;
394
+
395
+ function setActiveTab(tab) {
396
+ if (controlledTab == null) setInternalTab(tab);
397
+ onTabChange?.(tab);
398
+ }
399
+
400
+ const detectedFields = useMemo(
401
+ () => detectFieldIdsFromLastResponse(registryRow?.lastResponse),
402
+ [registryRow?.lastResponse]
403
+ );
404
+
405
+ if (!node) {
406
+ return (
407
+ <div className="dm-orchestration-config dm-orchestration-config--empty">
408
+ <p>Select a node on the canvas to configure it in this panel.</p>
409
+ </div>
410
+ );
411
+ }
412
+
413
+ const config = node.config || {};
414
+ const type = String(node.type || "");
415
+ const meta = config.requestHeadersMetadata || {};
416
+ const workspaceObjects = (Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [])
417
+ .filter((object) => object?.id && object?.objectType !== "sandbox-environment" && object?.objectType !== "api-registry");
418
+ const selectedObject = getSelectedObject(workspaceObjects, config.objectId);
419
+ const selectedObjectFields = getObjectFields(selectedObject);
420
+ const selectedObjectFieldIds = selectedObjectFields.map((field) => field.id);
421
+
422
+ function patchConfig(patch) {
423
+ onConfigChange?.({ ...config, ...patch });
424
+ }
425
+
426
+ const tabsForType = type === "api-registry-call" || type === "core-action"
427
+ ? ["configuration", "test", "advanced"]
428
+ : type === "input" || type === "transform-filter" || type === "data-action" || type === "data-trigger" || type === "ai-agent" || type === "flow-control" || type === "human-input"
429
+ ? ["configuration", "advanced"]
430
+ : ["configuration"];
431
+ const activeTab = tabsForType.includes(rawActiveTab) ? rawActiveTab : "configuration";
432
+
433
+ const registryConnected = isApiRegistryTestSuccessful(registryRow);
434
+ const responseMode = config.responseMode || config.mode || "json";
435
+
436
+ return (
437
+ <div className="dm-orchestration-config">
438
+ {tabsForType.length > 1 && (
439
+ <div className="dm-orchestration-config__tabs" role="tablist">
440
+ {tabsForType.map((tab) => (
441
+ <button
442
+ key={tab}
443
+ type="button"
444
+ role="tab"
445
+ aria-selected={activeTab === tab}
446
+ className={activeTab === tab ? "is-active" : ""}
447
+ onClick={() => setActiveTab(tab)}
448
+ >
449
+ {tab.charAt(0).toUpperCase() + tab.slice(1)}
450
+ </button>
451
+ ))}
452
+ </div>
453
+ )}
454
+
455
+ {activeTab === "configuration" && type === "input" && (
456
+ <div className="dm-orchestration-config__pane">
457
+ <label className="dm-orchestration-config__field">
458
+ <span>Input mode</span>
459
+ <select value={config.inputMode || "manual"} disabled={disabled} onChange={(e) => patchConfig({ inputMode: e.target.value })}>
460
+ <option value="manual">manual</option>
461
+ <option value="record">record</option>
462
+ <option value="source-record">source-record</option>
463
+ </select>
464
+ </label>
465
+ <PayloadKeyRows
466
+ payload={config.samplePayload}
467
+ disabled={disabled}
468
+ onChange={(samplePayload) => patchConfig({ samplePayload })}
469
+ />
470
+ <p className="dm-orchestration-config__hint">
471
+ Bind values with {"{{input.key}}"} in the API endpoint or body template.
472
+ </p>
473
+ </div>
474
+ )}
475
+
476
+ {activeTab === "configuration" && type === "api-registry-call" && (
477
+ <div className="dm-orchestration-config__pane">
478
+ {registryConnected && (
479
+ <span className="dm-orchestration-config__badge is-connected">Connected</span>
480
+ )}
481
+ <label className="dm-orchestration-config__field">
482
+ <span>Method</span>
483
+ <select
484
+ value={String(config.method || "GET").toUpperCase()}
485
+ disabled={disabled}
486
+ onChange={(e) => patchConfig({ method: e.target.value })}
487
+ >
488
+ {["GET", "POST", "PUT", "PATCH", "DELETE"].map((m) => (
489
+ <option key={m} value={m}>{m}</option>
490
+ ))}
491
+ </select>
492
+ </label>
493
+ <label className="dm-orchestration-config__field">
494
+ <span>Endpoint</span>
495
+ <input value={config.endpoint || ""} disabled={disabled} onChange={(e) => patchConfig({ endpoint: e.target.value })} />
496
+ </label>
497
+ <label className="dm-orchestration-config__field">
498
+ <span>Body template</span>
499
+ <textarea rows={3} value={config.bodyTemplate || ""} disabled={disabled} onChange={(e) => patchConfig({ bodyTemplate: e.target.value })} />
500
+ </label>
501
+ <label className="dm-orchestration-config__field">
502
+ <span>Auth reference</span>
503
+ <input value={config.authRef || ""} disabled={disabled} onChange={(e) => patchConfig({ authRef: e.target.value })} />
504
+ </label>
505
+ <label className="dm-orchestration-config__field">
506
+ <span>Auth header name</span>
507
+ <input
508
+ value={meta.authHeaderName || config.authHeaderName || ""}
509
+ disabled={disabled}
510
+ onChange={(e) => patchConfig({
511
+ authHeaderName: e.target.value,
512
+ requestHeadersMetadata: { ...meta, authHeaderName: e.target.value }
513
+ })}
514
+ />
515
+ </label>
516
+ <label className="dm-orchestration-config__field">
517
+ <span>Auth prefix</span>
518
+ <input
519
+ value={meta.authPrefix || config.authPrefix || ""}
520
+ disabled={disabled}
521
+ onChange={(e) => patchConfig({
522
+ authPrefix: e.target.value,
523
+ requestHeadersMetadata: { ...meta, authPrefix: e.target.value }
524
+ })}
525
+ />
526
+ </label>
527
+ </div>
528
+ )}
529
+
530
+ {activeTab === "configuration" && type === "transform-filter" && (
531
+ <div className="dm-orchestration-config__pane">
532
+ {detectedFields.length > 0 && (
533
+ <p className="dm-orchestration-config__meta">{detectedFields.length} fields detected from last API test</p>
534
+ )}
535
+ <label className="dm-orchestration-config__field">
536
+ <span>Root path</span>
537
+ <input
538
+ value={config.rootPath || ""}
539
+ placeholder="data.items"
540
+ disabled={disabled}
541
+ onChange={(e) => patchConfig({ rootPath: e.target.value })}
542
+ />
543
+ </label>
544
+ <label className="dm-orchestration-config__field">
545
+ <span>Response mode</span>
546
+ <select
547
+ value={responseMode}
548
+ disabled={disabled}
549
+ onChange={(e) => patchConfig({ responseMode: e.target.value, mode: e.target.value })}
550
+ >
551
+ <option value="json">json</option>
552
+ <option value="array">array</option>
553
+ <option value="object">object</option>
554
+ </select>
555
+ </label>
556
+ <FieldMapRows
557
+ fieldMap={config.fieldMap}
558
+ fieldOptions={detectedFields}
559
+ disabled={disabled}
560
+ onChange={(fieldMap) => patchConfig({ fieldMap })}
561
+ />
562
+ </div>
563
+ )}
564
+
565
+ {activeTab === "configuration" && type === "tool-result" && (
566
+ <div className="dm-orchestration-config__pane">
567
+ {registryRow?.status && (
568
+ <span className={`dm-orchestration-config__badge is-${String(registryRow.status).toLowerCase()}`}>
569
+ Latest registry test: {registryRow.status}
570
+ </span>
571
+ )}
572
+ <label className="dm-orchestration-config__field dm-orchestration-config__field-inline">
573
+ <input
574
+ type="checkbox"
575
+ checked={config.writeLastResponse !== false}
576
+ disabled={disabled}
577
+ onChange={(e) => patchConfig({ writeLastResponse: e.target.checked })}
578
+ />
579
+ <span>Write lastResponse on success</span>
580
+ </label>
581
+ <label className="dm-orchestration-config__field dm-orchestration-config__field-inline">
582
+ <input
583
+ type="checkbox"
584
+ checked={config.writeSourceRecord !== false}
585
+ disabled={disabled}
586
+ onChange={(e) => patchConfig({ writeSourceRecord: e.target.checked })}
587
+ />
588
+ <span>Write source record history</span>
589
+ </label>
590
+ <label className="dm-orchestration-config__field">
591
+ <span>Success HTTP codes</span>
592
+ <input
593
+ value={Array.isArray(config.successStatusCodes) ? config.successStatusCodes.join(", ") : "200"}
594
+ disabled={disabled}
595
+ onChange={(e) => {
596
+ const codes = e.target.value.split(",").map((v) => Number(v.trim())).filter(Number.isFinite);
597
+ patchConfig({ successStatusCodes: codes.length ? codes : [200] });
598
+ }}
599
+ />
600
+ </label>
601
+ <label className="dm-orchestration-config__field">
602
+ <span>Output mode</span>
603
+ <input value={config.outputMode || "normalized-json"} disabled={disabled} onChange={(e) => patchConfig({ outputMode: e.target.value })} />
604
+ </label>
605
+ {registryRow?.lastResponse && (
606
+ <div className="dm-orchestration-preview">
607
+ <span>Registry test preview</span>
608
+ <pre>{String(registryRow.lastResponse).slice(0, 400)}{String(registryRow.lastResponse).length > 400 ? "…" : ""}</pre>
609
+ </div>
610
+ )}
611
+ </div>
612
+ )}
613
+
614
+ {activeTab === "configuration" && type === "thinAdapter" && (
615
+ <div className="dm-orchestration-config__pane">
616
+ <div className="dm-orchestration-config__section">
617
+ <label className="dm-orchestration-config__field">
618
+ <span>Node id</span>
619
+ <input
620
+ value={node.id || ""}
621
+ disabled
622
+ />
623
+ </label>
624
+ <label className="dm-orchestration-config__field">
625
+ <span>Node type</span>
626
+ <input value="AI Model" disabled />
627
+ </label>
628
+ <label className="dm-orchestration-config__field">
629
+ <span>Model reference</span>
630
+ <input
631
+ value={node.sandbox || ""}
632
+ disabled={disabled}
633
+ onChange={(event) => patchConfig({ __nodePatch: { sandbox: event.target.value } })}
634
+ />
635
+ </label>
636
+ <label className="dm-orchestration-config__field">
637
+ <span>Execution policy</span>
638
+ <select
639
+ value={config.executionPolicy || "sequential"}
640
+ disabled={disabled}
641
+ onChange={(event) => patchConfig({ executionPolicy: event.target.value })}
642
+ >
643
+ <option value="sequential">sequential</option>
644
+ <option value="parallel">parallel</option>
645
+ <option value="conditional">conditional</option>
646
+ </select>
647
+ </label>
648
+ <label className="dm-orchestration-config__field">
649
+ <span>Input binding</span>
650
+ <input
651
+ value={config.inputBinding || ""}
652
+ placeholder="{{previous.output}}"
653
+ disabled={disabled}
654
+ onChange={(event) => patchConfig({ inputBinding: event.target.value })}
655
+ />
656
+ </label>
657
+ <label className="dm-orchestration-config__field">
658
+ <span>Output key</span>
659
+ <input
660
+ value={config.outputKey || ""}
661
+ placeholder="result"
662
+ disabled={disabled}
663
+ onChange={(event) => patchConfig({ outputKey: event.target.value })}
664
+ />
665
+ </label>
666
+ </div>
667
+
668
+ <VersionDeltaControls node={node} config={config} sandboxRow={sandboxRow} disabled={disabled} onChange={onConfigChange} />
669
+
670
+ <details className="dm-orchestration-config__advanced-json dm-orchestration-config__node-json">
671
+ <summary>Node JSON</summary>
672
+ <pre className="dm-orchestration-preview"><code>{JSON.stringify(node, null, 2)}</code></pre>
673
+ </details>
674
+ </div>
675
+ )}
676
+
677
+ {activeTab === "configuration" && (type === "data-action" || type === "data-trigger") && (
678
+ <div className="dm-orchestration-config__pane">
679
+ {config.destructive && (
680
+ <span className="dm-orchestration-config__badge is-failed">Double confirmation required</span>
681
+ )}
682
+ <label className="dm-orchestration-config__field">
683
+ <span>Workspace object</span>
684
+ <select
685
+ value={config.objectId || ""}
686
+ disabled={disabled}
687
+ onChange={(e) => {
688
+ const selected = workspaceObjects.find((object) => String(object.id) === e.target.value);
689
+ const objectName = String(selected?.name || selected?.label || selected?.id || "");
690
+ patchConfig({
691
+ objectId: e.target.value,
692
+ objectType: String(selected?.objectType || ""),
693
+ objectName,
694
+ __nodePatch: {
695
+ subtitle: objectName || "Select workspace object"
696
+ }
697
+ });
698
+ }}
699
+ >
700
+ <option value="">Select object</option>
701
+ {workspaceObjects.map((object) => (
702
+ <option key={object.id} value={object.id}>
703
+ {object.name || object.label || object.id}
704
+ </option>
705
+ ))}
706
+ </select>
707
+ </label>
708
+ <label className="dm-orchestration-config__field">
709
+ <span>Action</span>
710
+ <select value={config.action || node.id || ""} disabled={disabled} onChange={(e) => patchConfig({ action: e.target.value })}>
711
+ <option value="create-record">Create Record</option>
712
+ <option value="update-record">Update Record</option>
713
+ <option value="delete-record">Delete Record</option>
714
+ <option value="search-records">Search Records</option>
715
+ <option value="upsert-record">Create or Update Record</option>
716
+ <option value="record-created">Record is created</option>
717
+ <option value="record-updated">Record is updated</option>
718
+ <option value="record-deleted">Record is deleted</option>
719
+ </select>
720
+ </label>
721
+ {selectedObjectFields.length > 0 && (
722
+ <div className="dm-orchestration-config__section">
723
+ <span>Object fields</span>
724
+ {(config.action === "search-records" || config.action === "update-record" || config.action === "delete-record" || config.action === "upsert-record") && (
725
+ <FilterClauseList
726
+ filters={config.filters}
727
+ filterMode={config.filterMode}
728
+ disabled={disabled}
729
+ fieldOptions={selectedObjectFieldIds}
730
+ onChange={(filters, filterMode) => patchConfig({ filters, filterMode })}
731
+ />
732
+ )}
733
+ {(config.action === "create-record" || config.action === "update-record" || config.action === "upsert-record") && (
734
+ <FieldMapRows
735
+ fieldMap={config.fieldValues}
736
+ fieldOptions={selectedObjectFieldIds}
737
+ disabled={disabled}
738
+ onChange={(fieldValues) => patchConfig({ fieldValues })}
739
+ />
740
+ )}
741
+ {config.action === "search-records" && (
742
+ <>
743
+ <label className="dm-orchestration-config__field">
744
+ <span>Result limit</span>
745
+ <input type="number" min="1" value={config.limit ?? 25} disabled={disabled} onChange={(e) => patchConfig({ limit: Number(e.target.value) })} />
746
+ </label>
747
+ <label className="dm-orchestration-config__field">
748
+ <span>Sort field</span>
749
+ <select value={config.sortField || ""} disabled={disabled} onChange={(e) => patchConfig({ sortField: e.target.value })}>
750
+ <option value="">No sort</option>
751
+ {selectedObjectFields.map((field) => <option key={field.id} value={field.id}>{field.label}</option>)}
752
+ </select>
753
+ </label>
754
+ </>
755
+ )}
756
+ </div>
757
+ )}
758
+ <label className="dm-orchestration-config__field dm-orchestration-config__field-inline">
759
+ <input
760
+ type="checkbox"
761
+ checked={config.confirmationRequired === true}
762
+ disabled={disabled || config.destructive === true}
763
+ onChange={(e) => patchConfig({ confirmationRequired: e.target.checked })}
764
+ />
765
+ <span>Require confirmation before destructive or version-changing execution</span>
766
+ </label>
767
+ <p className="dm-orchestration-config__hint">
768
+ Data actions bind only to this workspace data model. Execution resolves the latest object schema at run time.
769
+ </p>
770
+ </div>
771
+ )}
772
+
773
+ {activeTab === "configuration" && type === "ai-agent" && (
774
+ <div className="dm-orchestration-config__pane">
775
+ <label className="dm-orchestration-config__field">
776
+ <span>Model</span>
777
+ <select value={config.model || MODEL_OPTIONS[0]} disabled={disabled} onChange={(e) => patchConfig({ model: e.target.value })}>
778
+ {MODEL_OPTIONS.map((model) => <option key={model} value={model}>{model}</option>)}
779
+ </select>
780
+ </label>
781
+ <label className="dm-orchestration-config__field">
782
+ <span>Input prompt</span>
783
+ <textarea
784
+ rows={4}
785
+ placeholder="Describe what you want the AI to do..."
786
+ value={config.prompt || ""}
787
+ disabled={disabled}
788
+ onChange={(e) => patchConfig({ prompt: e.target.value })}
789
+ />
790
+ </label>
791
+ <div className="dm-orchestration-config__section">
792
+ <span>Permissions</span>
793
+ <label className="dm-orchestration-config__field dm-orchestration-config__field-inline">
794
+ <input
795
+ type="checkbox"
796
+ checked={config.canReadWorkspace !== false}
797
+ disabled={disabled}
798
+ onChange={(e) => patchConfig({ canReadWorkspace: e.target.checked })}
799
+ />
800
+ <span>Read workspace data</span>
801
+ </label>
802
+ <label className="dm-orchestration-config__field dm-orchestration-config__field-inline">
803
+ <input
804
+ type="checkbox"
805
+ checked={config.canWriteDraft === true}
806
+ disabled={disabled}
807
+ onChange={(e) => patchConfig({ canWriteDraft: e.target.checked })}
808
+ />
809
+ <span>Write draft changes only</span>
810
+ </label>
811
+ <label className="dm-orchestration-config__field dm-orchestration-config__field-inline">
812
+ <input
813
+ type="checkbox"
814
+ checked={config.networkAccess === true}
815
+ disabled={disabled}
816
+ onChange={(e) => patchConfig({ networkAccess: e.target.checked })}
817
+ />
818
+ <span>Allow network access</span>
819
+ </label>
820
+ </div>
821
+ <KeyValueRows
822
+ label="Output fields"
823
+ entries={config.outputs}
824
+ disabled={disabled}
825
+ keyPlaceholder="Variable name"
826
+ valuePlaceholder="Instruction for AI"
827
+ onChange={(outputs) => patchConfig({ outputs })}
828
+ />
829
+ <label className="dm-orchestration-config__field">
830
+ <span>Default output type</span>
831
+ <select value={config.outputType || "Text"} disabled={disabled} onChange={(e) => patchConfig({ outputType: e.target.value })}>
832
+ {OUTPUT_TYPES.map((item) => <option key={item} value={item}>{item}</option>)}
833
+ </select>
834
+ </label>
835
+ </div>
836
+ )}
837
+
838
+ {activeTab === "configuration" && type === "flow-control" && (
839
+ <div className="dm-orchestration-config__pane">
840
+ {config.action === "iterator" && (
841
+ <>
842
+ <label className="dm-orchestration-config__field">
843
+ <span>Collection path</span>
844
+ <input placeholder="{{previous.records}}" value={config.collectionPath || ""} disabled={disabled} onChange={(e) => patchConfig({ collectionPath: e.target.value })} />
845
+ </label>
846
+ <label className="dm-orchestration-config__field">
847
+ <span>Item variable</span>
848
+ <input placeholder="item" value={config.itemVariable || "item"} disabled={disabled} onChange={(e) => patchConfig({ itemVariable: e.target.value })} />
849
+ </label>
850
+ <label className="dm-orchestration-config__field">
851
+ <span>Concurrency</span>
852
+ <input type="number" min="1" value={config.concurrency ?? 1} disabled={disabled} onChange={(e) => patchConfig({ concurrency: Number(e.target.value) })} />
853
+ </label>
854
+ </>
855
+ )}
856
+ {config.action === "filter" && (
857
+ <FilterClauseList
858
+ filters={config.filters}
859
+ filterMode={config.filterMode}
860
+ disabled={disabled}
861
+ fieldOptions={detectedFields}
862
+ onChange={(filters, filterMode) => patchConfig({ filters, filterMode })}
863
+ />
864
+ )}
865
+ {config.action === "if-else" && (
866
+ <>
867
+ <FilterClauseList
868
+ filters={config.conditions}
869
+ filterMode={config.conditionMode}
870
+ disabled={disabled}
871
+ fieldOptions={detectedFields}
872
+ onChange={(conditions, conditionMode) => patchConfig({ conditions, conditionMode })}
873
+ />
874
+ <label className="dm-orchestration-config__field">
875
+ <span>True branch label</span>
876
+ <input value={config.trueLabel || "Yes"} disabled={disabled} onChange={(e) => patchConfig({ trueLabel: e.target.value })} />
877
+ </label>
878
+ <label className="dm-orchestration-config__field">
879
+ <span>False branch label</span>
880
+ <input value={config.falseLabel || "No"} disabled={disabled} onChange={(e) => patchConfig({ falseLabel: e.target.value })} />
881
+ </label>
882
+ </>
883
+ )}
884
+ {config.action === "delay" && (
885
+ <>
886
+ <label className="dm-orchestration-config__field">
887
+ <span>Delay amount</span>
888
+ <input type="number" min="1" value={config.delayAmount ?? 5} disabled={disabled} onChange={(e) => patchConfig({ delayAmount: Number(e.target.value) })} />
889
+ </label>
890
+ <label className="dm-orchestration-config__field">
891
+ <span>Delay unit</span>
892
+ <select value={config.delayUnit || "minutes"} disabled={disabled} onChange={(e) => patchConfig({ delayUnit: e.target.value })}>
893
+ <option value="seconds">seconds</option>
894
+ <option value="minutes">minutes</option>
895
+ <option value="hours">hours</option>
896
+ <option value="days">days</option>
897
+ </select>
898
+ </label>
899
+ </>
900
+ )}
901
+ </div>
902
+ )}
903
+
904
+ {activeTab === "configuration" && type === "core-action" && (
905
+ <div className="dm-orchestration-config__pane">
906
+ {(config.action === "http-request" || node.id === "http-request") && (
907
+ <>
908
+ <label className="dm-orchestration-config__field">
909
+ <span>URL</span>
910
+ <input placeholder="https://api.example.com/endpoint" value={config.url || ""} disabled={disabled} onChange={(e) => patchConfig({ url: e.target.value })} />
911
+ </label>
912
+ <label className="dm-orchestration-config__field">
913
+ <span>HTTP Method</span>
914
+ <select value={String(config.method || "GET").toUpperCase()} disabled={disabled} onChange={(e) => patchConfig({ method: e.target.value })}>
915
+ {HTTP_METHODS.map((method) => <option key={method} value={method}>{method}</option>)}
916
+ </select>
917
+ </label>
918
+ <KeyValueRows
919
+ label="Headers Input"
920
+ entries={config.headers}
921
+ disabled={disabled}
922
+ keyPlaceholder="Header name"
923
+ valuePlaceholder="Header value"
924
+ onChange={(headers) => patchConfig({ headers })}
925
+ />
926
+ <label className="dm-orchestration-config__field">
927
+ <span>Request Body</span>
928
+ <textarea rows={4} placeholder="{ }" value={config.body || ""} disabled={disabled} onChange={(e) => patchConfig({ body: e.target.value })} />
929
+ </label>
930
+ <label className="dm-orchestration-config__field">
931
+ <span>Expected Response Body</span>
932
+ <textarea
933
+ rows={5}
934
+ placeholder={'{\n "id": "123",\n "status": "ok"\n}'}
935
+ value={config.expectedResponseBody || ""}
936
+ disabled={disabled}
937
+ onChange={(e) => patchConfig({ expectedResponseBody: e.target.value })}
938
+ />
939
+ </label>
940
+ </>
941
+ )}
942
+ {(config.action === "send-email" || config.action === "draft-email") && (
943
+ <>
944
+ <label className="dm-orchestration-config__field">
945
+ <span>To</span>
946
+ <input placeholder="{{record.email}}" value={config.to || ""} disabled={disabled} onChange={(e) => patchConfig({ to: e.target.value })} />
947
+ </label>
948
+ <label className="dm-orchestration-config__field">
949
+ <span>Subject</span>
950
+ <input value={config.subject || ""} disabled={disabled} onChange={(e) => patchConfig({ subject: e.target.value })} />
951
+ </label>
952
+ <label className="dm-orchestration-config__field">
953
+ <span>Message</span>
954
+ <textarea rows={6} value={config.message || ""} disabled={disabled} onChange={(e) => patchConfig({ message: e.target.value })} />
955
+ </label>
956
+ <label className="dm-orchestration-config__field dm-orchestration-config__field-inline">
957
+ <input type="checkbox" checked={config.requireApproval !== false} disabled={disabled} onChange={(e) => patchConfig({ requireApproval: e.target.checked })} />
958
+ <span>Require approval before sending</span>
959
+ </label>
960
+ </>
961
+ )}
962
+ {config.action === "code-function" && (
963
+ <>
964
+ <label className="dm-orchestration-config__field">
965
+ <span>Function name</span>
966
+ <input value={config.functionName || "logicFunction"} disabled={disabled} onChange={(e) => patchConfig({ functionName: e.target.value })} />
967
+ </label>
968
+ <label className="dm-orchestration-config__field">
969
+ <span>Code</span>
970
+ <textarea rows={8} placeholder="return input;" value={config.code || ""} disabled={disabled} onChange={(e) => patchConfig({ code: e.target.value })} />
971
+ </label>
972
+ <KeyValueRows label="Environment references" entries={config.envRefs} disabled={disabled} onChange={(envRefs) => patchConfig({ envRefs })} />
973
+ </>
974
+ )}
975
+ </div>
976
+ )}
977
+
978
+ {activeTab === "configuration" && type === "human-input" && (
979
+ <div className="dm-orchestration-config__pane">
980
+ <label className="dm-orchestration-config__field">
981
+ <span>Form title</span>
982
+ <input value={config.title || "Review input"} disabled={disabled} onChange={(e) => patchConfig({ title: e.target.value })} />
983
+ </label>
984
+ <label className="dm-orchestration-config__field">
985
+ <span>Instructions</span>
986
+ <textarea rows={4} value={config.instructions || ""} disabled={disabled} onChange={(e) => patchConfig({ instructions: e.target.value })} />
987
+ </label>
988
+ <KeyValueRows
989
+ label="Form fields"
990
+ entries={config.fields}
991
+ disabled={disabled}
992
+ keyPlaceholder="Field name"
993
+ valuePlaceholder="Field type or help text"
994
+ onChange={(fields) => patchConfig({ fields })}
995
+ />
996
+ <label className="dm-orchestration-config__field dm-orchestration-config__field-inline">
997
+ <input type="checkbox" checked={config.required !== false} disabled={disabled} onChange={(e) => patchConfig({ required: e.target.checked })} />
998
+ <span>Require response before continuing</span>
999
+ </label>
1000
+ </div>
1001
+ )}
1002
+
1003
+ {activeTab === "test" && (
1004
+ <div className="dm-orchestration-config__pane">
1005
+ <label className="dm-orchestration-config__field">
1006
+ <span>Success condition</span>
1007
+ <input value={config.successCondition || "HTTP 200 or successful execution"} disabled={disabled} onChange={(e) => patchConfig({ successCondition: e.target.value })} />
1008
+ </label>
1009
+ <label className="dm-orchestration-config__field">
1010
+ <span>Sample response</span>
1011
+ <textarea rows={5} value={config.sampleResponse || ""} disabled={disabled} onChange={(e) => patchConfig({ sampleResponse: e.target.value })} />
1012
+ </label>
1013
+ <label className="dm-orchestration-config__field dm-orchestration-config__field-inline">
1014
+ <input type="checkbox" checked={config.blockPublishOnFailure !== false} disabled={disabled} onChange={(e) => patchConfig({ blockPublishOnFailure: e.target.checked })} />
1015
+ <span>Block Publish unless this step passes</span>
1016
+ </label>
1017
+ </div>
1018
+ )}
1019
+
1020
+ {activeTab === "advanced" && (type === "data-action" || type === "data-trigger" || type === "ai-agent" || type === "flow-control" || type === "core-action" || type === "human-input") && (
1021
+ <div className="dm-orchestration-config__pane">
1022
+ <details className="dm-orchestration-config__advanced-json" open>
1023
+ <summary>Advanced JSON</summary>
1024
+ <pre className="dm-orchestration-preview">{JSON.stringify(config, null, 2)}</pre>
1025
+ </details>
1026
+ </div>
1027
+ )}
1028
+
1029
+ {activeTab === "filters" && (type === "input" || type === "transform-filter") && (
1030
+ <FilterClauseList
1031
+ filters={config.filters}
1032
+ filterMode={config.filterMode}
1033
+ disabled={disabled}
1034
+ fieldOptions={type === "transform-filter" ? detectedFields : []}
1035
+ onChange={(filters, filterMode) => patchConfig({ filters, filterMode })}
1036
+ />
1037
+ )}
1038
+
1039
+ {activeTab === "preview" && (
1040
+ <div className="dm-orchestration-config__pane">
1041
+ {type === "thinAdapter" ? (
1042
+ <>
1043
+ <ul className="dm-orchestration-config__preview-list">
1044
+ <li>Sandbox: {node.sandbox || node.id || "unknown"}</li>
1045
+ <li>Type: thinAdapter</li>
1046
+ </ul>
1047
+ <details className="dm-orchestration-config__advanced-json dm-orchestration-config__node-json">
1048
+ <summary>Node JSON</summary>
1049
+ <pre className="dm-orchestration-preview"><code>{JSON.stringify(node, null, 2)}</code></pre>
1050
+ </details>
1051
+ </>
1052
+ ) : type === "tool-result" ? (
1053
+ <>
1054
+ <p className="dm-orchestration-config__hint">
1055
+ After Run sandbox, status, lastTested, and lastResponse update on the sandbox row.
1056
+ </p>
1057
+ <ul className="dm-orchestration-config__preview-list">
1058
+ <li>Success codes: {Array.isArray(config.successStatusCodes) ? config.successStatusCodes.join(", ") : "200"}</li>
1059
+ <li>Write lastResponse: {config.writeLastResponse !== false ? "yes" : "no"}</li>
1060
+ <li>Write source record: {config.writeSourceRecord !== false ? "yes" : "no"}</li>
1061
+ <li>Output mode: {config.outputMode || "normalized-json"}</li>
1062
+ </ul>
1063
+ {registryRow?.lastResponse && (
1064
+ <div className="dm-orchestration-preview">
1065
+ <span>Latest API test output (preview)</span>
1066
+ <pre>{String(registryRow.lastResponse).slice(0, 500)}{String(registryRow.lastResponse).length > 500 ? "…" : ""}</pre>
1067
+ </div>
1068
+ )}
1069
+ </>
1070
+ ) : (
1071
+ <p className="dm-orchestration-config__hint">Advanced node configuration preview.</p>
1072
+ )}
1073
+ <details className="dm-orchestration-config__advanced-json">
1074
+ <summary>Advanced JSON</summary>
1075
+ <pre className="dm-orchestration-preview">{JSON.stringify(config, null, 2)}</pre>
1076
+ </details>
1077
+ </div>
1078
+ )}
1079
+
1080
+ {activeTab === "advanced" && (
1081
+ <div className="dm-orchestration-config__pane">
1082
+ {type === "api-registry-call" && (
1083
+ <>
1084
+ <label className="dm-orchestration-config__field">
1085
+ <span>Timeout (ms)</span>
1086
+ <input
1087
+ type="number"
1088
+ value={config.timeoutMs ?? 30000}
1089
+ disabled={disabled}
1090
+ onChange={(e) => patchConfig({ timeoutMs: Number(e.target.value) })}
1091
+ />
1092
+ </label>
1093
+ <label className="dm-orchestration-config__field">
1094
+ <span>Query params (JSON object)</span>
1095
+ <textarea
1096
+ rows={2}
1097
+ disabled={disabled}
1098
+ value={JSON.stringify(config.queryParams || {}, null, 2)}
1099
+ onChange={(e) => {
1100
+ try {
1101
+ patchConfig({ queryParams: JSON.parse(e.target.value || "{}") });
1102
+ } catch {
1103
+ /* keep typing */
1104
+ }
1105
+ }}
1106
+ />
1107
+ </label>
1108
+ </>
1109
+ )}
1110
+ {type === "input" && (
1111
+ <>
1112
+ <label className="dm-orchestration-config__field">
1113
+ <span>Source type</span>
1114
+ <input value={config.sourceType || ""} disabled={disabled} onChange={(e) => patchConfig({ sourceType: e.target.value })} />
1115
+ </label>
1116
+ <label className="dm-orchestration-config__field">
1117
+ <span>Source ID</span>
1118
+ <input value={config.sourceId || ""} disabled={disabled} onChange={(e) => patchConfig({ sourceId: e.target.value })} />
1119
+ </label>
1120
+ <label className="dm-orchestration-config__field">
1121
+ <span>Entity ID</span>
1122
+ <input value={config.entityId || ""} disabled={disabled} onChange={(e) => patchConfig({ entityId: e.target.value })} />
1123
+ </label>
1124
+ </>
1125
+ )}
1126
+ {type === "transform-filter" && (
1127
+ <>
1128
+ <label className="dm-orchestration-config__field">
1129
+ <span>Max rows (0 = no limit)</span>
1130
+ <input
1131
+ type="number"
1132
+ value={config.maxRows ?? 0}
1133
+ disabled={disabled}
1134
+ onChange={(e) => patchConfig({ maxRows: Number(e.target.value) })}
1135
+ />
1136
+ </label>
1137
+ <label className="dm-orchestration-config__field">
1138
+ <span>Include fields (comma-separated)</span>
1139
+ <input
1140
+ value={Array.isArray(config.includeFields) ? config.includeFields.join(", ") : ""}
1141
+ disabled={disabled}
1142
+ onChange={(e) => patchConfig({
1143
+ includeFields: e.target.value.split(",").map((s) => s.trim()).filter(Boolean)
1144
+ })}
1145
+ />
1146
+ </label>
1147
+ <label className="dm-orchestration-config__field">
1148
+ <span>Exclude fields (comma-separated)</span>
1149
+ <input
1150
+ value={Array.isArray(config.excludeFields) ? config.excludeFields.join(", ") : ""}
1151
+ disabled={disabled}
1152
+ onChange={(e) => patchConfig({
1153
+ excludeFields: e.target.value.split(",").map((s) => s.trim()).filter(Boolean)
1154
+ })}
1155
+ />
1156
+ </label>
1157
+ </>
1158
+ )}
1159
+ {type === "tool-result" && (
1160
+ <>
1161
+ <label className="dm-orchestration-config__field">
1162
+ <span>Preview fields (comma-separated)</span>
1163
+ <input
1164
+ value={Array.isArray(config.previewFields) ? config.previewFields.join(", ") : ""}
1165
+ disabled={disabled}
1166
+ onChange={(e) => patchConfig({
1167
+ previewFields: e.target.value.split(",").map((s) => s.trim()).filter(Boolean)
1168
+ })}
1169
+ />
1170
+ </label>
1171
+ <label className="dm-orchestration-config__field">
1172
+ <span>Status field name</span>
1173
+ <input value={config.statusField || "status"} disabled={disabled} onChange={(e) => patchConfig({ statusField: e.target.value })} />
1174
+ </label>
1175
+ <label className="dm-orchestration-config__field">
1176
+ <span>Last tested field name</span>
1177
+ <input value={config.lastTestedField || "lastTested"} disabled={disabled} onChange={(e) => patchConfig({ lastTestedField: e.target.value })} />
1178
+ </label>
1179
+ </>
1180
+ )}
1181
+ {(type === "input" || type === "transform-filter") && (
1182
+ <details className="dm-orchestration-config__advanced-json">
1183
+ <summary>Advanced JSON</summary>
1184
+ <pre className="dm-orchestration-preview">{JSON.stringify(config, null, 2)}</pre>
1185
+ </details>
1186
+ )}
1187
+ </div>
1188
+ )}
1189
+
1190
+ {activeTab === "configuration" && type !== "thinAdapter" && (
1191
+ <VersionDeltaControls node={node} config={config} sandboxRow={sandboxRow} disabled={disabled} onChange={onConfigChange} />
1192
+ )}
1193
+ <div className="dm-workflow-node-config-foot">
1194
+ <button type="button" className="dm-workflow-node-options" disabled={disabled}>
1195
+ Options ⌘O
1196
+ </button>
1197
+ <button type="button" className="dm-workflow-node-delete" disabled={disabled} onClick={onDeleteNode}>
1198
+ Delete
1199
+ </button>
1200
+ </div>
1201
+ </div>
1202
+ );
1203
+ }