@particle-academy/fancy-flow 0.65.2 → 0.66.0
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/README.md +100 -3
- package/dist/{FlowViewer-BI_AyyvU.d.ts → FlowViewer-BCc_BvuD.d.ts} +1 -1
- package/dist/{FlowViewer-DABx-6A-.d.cts → FlowViewer-CoAJ3o52.d.cts} +1 -1
- package/dist/{HumanPrompt-Bpp3JiVH.d.cts → HumanPrompt--pXDOyYC.d.cts} +2 -2
- package/dist/{HumanPrompt-BKThiyjB.d.ts → HumanPrompt-DXm_FP9z.d.ts} +2 -2
- package/dist/chunk-2QXKDLGT.js +58 -0
- package/dist/chunk-2QXKDLGT.js.map +1 -0
- package/dist/{chunk-77V4QC6Y.js → chunk-B7HKJJVV.js} +3 -3
- package/dist/{chunk-77V4QC6Y.js.map → chunk-B7HKJJVV.js.map} +1 -1
- package/dist/{chunk-MBVX4ZRB.js → chunk-FNYLKSFJ.js} +3 -3
- package/dist/{chunk-MBVX4ZRB.js.map → chunk-FNYLKSFJ.js.map} +1 -1
- package/dist/{chunk-JF6WCRBU.js → chunk-HQGOFGAL.js} +861 -40
- package/dist/chunk-HQGOFGAL.js.map +1 -0
- package/dist/{chunk-5H54OTKT.js → chunk-IEYCNFXZ.js} +3 -3
- package/dist/{chunk-5H54OTKT.js.map → chunk-IEYCNFXZ.js.map} +1 -1
- package/dist/{chunk-W5DPKJY4.js → chunk-MTONBM53.js} +4 -4
- package/dist/chunk-MTONBM53.js.map +1 -0
- package/dist/{chunk-RIAFHQT5.js → chunk-PNHZHEWE.js} +3 -3
- package/dist/{chunk-RIAFHQT5.js.map → chunk-PNHZHEWE.js.map} +1 -1
- package/dist/{chunk-EO6444T2.js → chunk-UCCTXJXL.js} +4 -4
- package/dist/{chunk-EO6444T2.js.map → chunk-UCCTXJXL.js.map} +1 -1
- package/dist/connectors.d.cts +2 -2
- package/dist/connectors.d.ts +2 -2
- package/dist/durable/index.d.cts +2 -2
- package/dist/durable/index.d.ts +2 -2
- package/dist/durable.cjs +837 -47
- package/dist/durable.cjs.map +1 -1
- package/dist/durable.js +2 -3
- package/dist/durable.js.map +1 -1
- package/dist/engine.cjs +843 -132
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.cts +6 -7
- package/dist/engine.d.ts +6 -7
- package/dist/engine.js +5 -6
- package/dist/engine.js.map +1 -1
- package/dist/fields/react-fancy.d.cts +3 -3
- package/dist/fields/react-fancy.d.ts +3 -3
- package/dist/index.cjs +853 -140
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -36
- package/dist/index.d.ts +65 -36
- package/dist/index.js +19 -18
- package/dist/index.js.map +1 -1
- package/dist/layout/index.d.cts +1 -1
- package/dist/layout/index.d.ts +1 -1
- package/dist/llm/prism.cjs.map +1 -1
- package/dist/llm/prism.d.cts +1 -2
- package/dist/llm/prism.d.ts +1 -2
- package/dist/llm/prism.js +1 -1
- package/dist/llm/vercel-ai.cjs.map +1 -1
- package/dist/llm/vercel-ai.d.cts +1 -2
- package/dist/llm/vercel-ai.d.ts +1 -2
- package/dist/llm/vercel-ai.js +1 -1
- package/dist/registry/index.d.cts +5 -6
- package/dist/registry/index.d.ts +5 -6
- package/dist/{registry-CntAdUcY.d.cts → registry-BcgllSk2.d.cts} +1 -1
- package/dist/{registry-DYoV-MQi.d.ts → registry-h-2QswZ5.d.ts} +1 -1
- package/dist/registry.cjs +867 -37
- package/dist/registry.cjs.map +1 -1
- package/dist/registry.js +3 -3
- package/dist/{run-cohort-C1nRh_mi.d.ts → run-cohort-CjUBWg6X.d.ts} +2 -2
- package/dist/{run-cohort-CBRAUj-z.d.cts → run-cohort-DL9JzzPU.d.cts} +2 -2
- package/dist/{run-flow-2XsHBwpd.d.cts → run-flow-B_8hgO5_.d.cts} +1 -1
- package/dist/{run-flow-D7AOCcfE.d.ts → run-flow-CxEGBOxd.d.ts} +1 -1
- package/dist/runtime/index.d.cts +5 -5
- package/dist/runtime/index.d.ts +5 -5
- package/dist/runtime.cjs +837 -36
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +4 -4
- package/dist/schema/index.d.cts +19 -6
- package/dist/schema/index.d.ts +19 -6
- package/dist/schema.cjs +837 -36
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.js +4 -4
- package/dist/screens.cjs +837 -36
- package/dist/screens.cjs.map +1 -1
- package/dist/screens.d.cts +2 -2
- package/dist/screens.d.ts +2 -2
- package/dist/screens.js +5 -5
- package/dist/terminal/fancy-term-host.cjs +99 -0
- package/dist/terminal/fancy-term-host.cjs.map +1 -0
- package/dist/terminal/fancy-term-host.d.cts +76 -0
- package/dist/terminal/fancy-term-host.d.ts +76 -0
- package/dist/terminal/fancy-term-host.js +92 -0
- package/dist/terminal/fancy-term-host.js.map +1 -0
- package/dist/types-B-Syk9-M.d.cts +620 -0
- package/dist/types-B-Syk9-M.d.ts +620 -0
- package/dist/{types-D71SKA5A.d.cts → types-C2ATTmjN.d.cts} +1 -1
- package/dist/{types-Ckhz-YwC.d.ts → types-VunLGqrn.d.ts} +1 -1
- package/dist/ux.cjs +837 -36
- package/dist/ux.cjs.map +1 -1
- package/dist/ux.d.cts +2 -2
- package/dist/ux.d.ts +2 -2
- package/dist/ux.js +2 -2
- package/package.json +11 -1
- package/dist/capabilities-BYa5p5jw.d.cts +0 -112
- package/dist/capabilities-COOXRiNL.d.ts +0 -112
- package/dist/chunk-JF6WCRBU.js.map +0 -1
- package/dist/chunk-UM4C46AF.js +0 -103
- package/dist/chunk-UM4C46AF.js.map +0 -1
- package/dist/chunk-USL4FMFU.js +0 -41
- package/dist/chunk-USL4FMFU.js.map +0 -1
- package/dist/chunk-W5DPKJY4.js.map +0 -1
- package/dist/types-JFYjPJAG.d.cts +0 -333
- package/dist/types-JFYjPJAG.d.ts +0 -333
package/dist/index.cjs
CHANGED
|
@@ -4314,9 +4314,9 @@ function transition_default() {
|
|
|
4314
4314
|
// node_modules/d3-transition/src/transition/end.js
|
|
4315
4315
|
function end_default() {
|
|
4316
4316
|
var on0, on1, that = this, id2 = that._id, size = that.size();
|
|
4317
|
-
return new Promise(function(
|
|
4317
|
+
return new Promise(function(resolve2, reject) {
|
|
4318
4318
|
var cancel = { value: reject }, end = { value: function() {
|
|
4319
|
-
if (--size === 0)
|
|
4319
|
+
if (--size === 0) resolve2();
|
|
4320
4320
|
} };
|
|
4321
4321
|
that.each(function() {
|
|
4322
4322
|
var schedule = set2(this, id2), on2 = schedule.on;
|
|
@@ -4328,7 +4328,7 @@ function end_default() {
|
|
|
4328
4328
|
}
|
|
4329
4329
|
schedule.on = on1;
|
|
4330
4330
|
});
|
|
4331
|
-
if (size === 0)
|
|
4331
|
+
if (size === 0) resolve2();
|
|
4332
4332
|
});
|
|
4333
4333
|
}
|
|
4334
4334
|
|
|
@@ -5289,13 +5289,13 @@ function areSetsEqual(a, b) {
|
|
|
5289
5289
|
return true;
|
|
5290
5290
|
}
|
|
5291
5291
|
function withResolvers() {
|
|
5292
|
-
let
|
|
5292
|
+
let resolve2;
|
|
5293
5293
|
let reject;
|
|
5294
5294
|
const promise = new Promise((res, rej) => {
|
|
5295
|
-
|
|
5295
|
+
resolve2 = res;
|
|
5296
5296
|
reject = rej;
|
|
5297
5297
|
});
|
|
5298
|
-
return { promise, resolve, reject };
|
|
5298
|
+
return { promise, resolve: resolve2, reject };
|
|
5299
5299
|
}
|
|
5300
5300
|
function mergeAriaLabelConfig(partial) {
|
|
5301
5301
|
return { ...defaultAriaLabelConfig, ...partial || {} };
|
|
@@ -6967,8 +6967,8 @@ function XYPanZoom({ domNode, minZoom, maxZoom, translateExtent, viewport, onPan
|
|
|
6967
6967
|
d3ZoomInstance.wheelDelta(wheelDelta);
|
|
6968
6968
|
async function setTransform(transform2, options) {
|
|
6969
6969
|
if (d3Selection) {
|
|
6970
|
-
return new Promise((
|
|
6971
|
-
d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).transform(getD3Transition(d3Selection, options?.duration, options?.ease, () =>
|
|
6970
|
+
return new Promise((resolve2) => {
|
|
6971
|
+
d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).transform(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve2(true)), transform2);
|
|
6972
6972
|
});
|
|
6973
6973
|
}
|
|
6974
6974
|
return false;
|
|
@@ -7070,16 +7070,16 @@ function XYPanZoom({ domNode, minZoom, maxZoom, translateExtent, viewport, onPan
|
|
|
7070
7070
|
}
|
|
7071
7071
|
async function scaleTo(zoom, options) {
|
|
7072
7072
|
if (d3Selection) {
|
|
7073
|
-
return new Promise((
|
|
7074
|
-
d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleTo(getD3Transition(d3Selection, options?.duration, options?.ease, () =>
|
|
7073
|
+
return new Promise((resolve2) => {
|
|
7074
|
+
d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleTo(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve2(true)), zoom);
|
|
7075
7075
|
});
|
|
7076
7076
|
}
|
|
7077
7077
|
return false;
|
|
7078
7078
|
}
|
|
7079
7079
|
async function scaleBy(factor, options) {
|
|
7080
7080
|
if (d3Selection) {
|
|
7081
|
-
return new Promise((
|
|
7082
|
-
d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleBy(getD3Transition(d3Selection, options?.duration, options?.ease, () =>
|
|
7081
|
+
return new Promise((resolve2) => {
|
|
7082
|
+
d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleBy(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve2(true)), factor);
|
|
7083
7083
|
});
|
|
7084
7084
|
}
|
|
7085
7085
|
return false;
|
|
@@ -10944,11 +10944,11 @@ function NoteNodeInner(props) {
|
|
|
10944
10944
|
const config = data.config ?? {};
|
|
10945
10945
|
const api = useFlowEditorOptional();
|
|
10946
10946
|
const hasConfigText = typeof config.text === "string" && config.text.length > 0;
|
|
10947
|
-
const
|
|
10947
|
+
const text3 = hasConfigText ? String(config.text) : typeof data.body === "string" ? data.body : typeof data.text === "string" ? data.text : "";
|
|
10948
10948
|
const title = typeof config.title === "string" && config.title || (!hasConfigText && typeof data.body === "string" && typeof data.label === "string" ? data.label : "");
|
|
10949
10949
|
const skin = NOTE_COLORS[coerceColor(config.color)];
|
|
10950
10950
|
const [editing, setEditing] = ReactExports.useState(false);
|
|
10951
|
-
const [draft, setDraft] = ReactExports.useState(
|
|
10951
|
+
const [draft, setDraft] = ReactExports.useState(text3);
|
|
10952
10952
|
const areaRef = ReactExports.useRef(null);
|
|
10953
10953
|
ReactExports.useEffect(() => {
|
|
10954
10954
|
if (editing && areaRef.current) {
|
|
@@ -10959,7 +10959,7 @@ function NoteNodeInner(props) {
|
|
|
10959
10959
|
}, [editing]);
|
|
10960
10960
|
const commit = () => {
|
|
10961
10961
|
setEditing(false);
|
|
10962
|
-
if (!api || draft ===
|
|
10962
|
+
if (!api || draft === text3) return;
|
|
10963
10963
|
const current = api.nodes.find((n) => n.id === props.id);
|
|
10964
10964
|
if (!current) return;
|
|
10965
10965
|
api.updateNode({
|
|
@@ -10973,7 +10973,7 @@ function NoteNodeInner(props) {
|
|
|
10973
10973
|
className: ["ff-note", props.selected ? "ff-note--selected" : ""].filter(Boolean).join(" "),
|
|
10974
10974
|
style: { background: skin.bg, borderColor: skin.border },
|
|
10975
10975
|
onDoubleClick: api ? () => {
|
|
10976
|
-
setDraft(
|
|
10976
|
+
setDraft(text3);
|
|
10977
10977
|
setEditing(true);
|
|
10978
10978
|
} : void 0,
|
|
10979
10979
|
children: [
|
|
@@ -10992,7 +10992,7 @@ function NoteNodeInner(props) {
|
|
|
10992
10992
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) commit();
|
|
10993
10993
|
}
|
|
10994
10994
|
}
|
|
10995
|
-
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-note__body", children:
|
|
10995
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-note__body", children: text3 || /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-note__placeholder", children: api ? "Double-click to write a note\u2026" : "Note" }) })
|
|
10996
10996
|
]
|
|
10997
10997
|
}
|
|
10998
10998
|
);
|
|
@@ -11193,6 +11193,239 @@ var RunIdentity = class _RunIdentity {
|
|
|
11193
11193
|
}
|
|
11194
11194
|
};
|
|
11195
11195
|
|
|
11196
|
+
// src/runtime/terminal-transcript.ts
|
|
11197
|
+
var ESC = String.fromCharCode(27);
|
|
11198
|
+
var CSI = String.fromCharCode(155);
|
|
11199
|
+
var BEL = String.fromCharCode(7);
|
|
11200
|
+
var ANSI = new RegExp(
|
|
11201
|
+
`[${ESC}${CSI}](?:\\[[0-?]*[ -/]*[@-~]|\\][^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)|[@-Z\\\\-_])`,
|
|
11202
|
+
"g"
|
|
11203
|
+
);
|
|
11204
|
+
var MAX_PENDING_ESCAPE = 1024;
|
|
11205
|
+
var MAX_BUFFER = 1e6;
|
|
11206
|
+
var TerminalTranscript = class {
|
|
11207
|
+
constructor() {
|
|
11208
|
+
/** Stripped, complete text not yet consumed by a wait. */
|
|
11209
|
+
this.text = "";
|
|
11210
|
+
/** Raw bytes held back because they may be the start of an escape sequence. */
|
|
11211
|
+
this.tail = "";
|
|
11212
|
+
this.waiters = /* @__PURE__ */ new Set();
|
|
11213
|
+
}
|
|
11214
|
+
/** Feed raw terminal output in. Safe to call with any chunking. */
|
|
11215
|
+
append(chunk) {
|
|
11216
|
+
this.tail += chunk;
|
|
11217
|
+
const safe = this.safeLength();
|
|
11218
|
+
if (safe > 0) {
|
|
11219
|
+
this.text += this.tail.slice(0, safe).replace(ANSI, "");
|
|
11220
|
+
this.tail = this.tail.slice(safe);
|
|
11221
|
+
}
|
|
11222
|
+
if (this.text.length > MAX_BUFFER) {
|
|
11223
|
+
this.text = this.text.slice(this.text.length - MAX_BUFFER);
|
|
11224
|
+
}
|
|
11225
|
+
for (const wake2 of [...this.waiters]) wake2();
|
|
11226
|
+
}
|
|
11227
|
+
/**
|
|
11228
|
+
* How much of `tail` can be stripped now.
|
|
11229
|
+
*
|
|
11230
|
+
* Everything, unless the final `ESC` has not yet been terminated — in which
|
|
11231
|
+
* case processing stops there and resumes when the rest arrives.
|
|
11232
|
+
*/
|
|
11233
|
+
safeLength() {
|
|
11234
|
+
const lastEsc = Math.max(this.tail.lastIndexOf(ESC), this.tail.lastIndexOf(CSI));
|
|
11235
|
+
if (lastEsc === -1) return this.tail.length;
|
|
11236
|
+
ANSI.lastIndex = lastEsc;
|
|
11237
|
+
const match = ANSI.exec(this.tail);
|
|
11238
|
+
ANSI.lastIndex = 0;
|
|
11239
|
+
if (match && match.index === lastEsc) return this.tail.length;
|
|
11240
|
+
if (this.tail.length - lastEsc > MAX_PENDING_ESCAPE) return this.tail.length;
|
|
11241
|
+
return lastEsc;
|
|
11242
|
+
}
|
|
11243
|
+
/** Unconsumed output, escape sequences removed. */
|
|
11244
|
+
peek() {
|
|
11245
|
+
return this.text;
|
|
11246
|
+
}
|
|
11247
|
+
/** Drop everything currently buffered — used before typing a new command. */
|
|
11248
|
+
clear() {
|
|
11249
|
+
this.text = "";
|
|
11250
|
+
this.tail = "";
|
|
11251
|
+
}
|
|
11252
|
+
/**
|
|
11253
|
+
* Wait until `pattern` matches the unconsumed text.
|
|
11254
|
+
*
|
|
11255
|
+
* Checks what has ALREADY arrived before subscribing, because the common case
|
|
11256
|
+
* is that the process answered while the previous node was still finishing.
|
|
11257
|
+
*
|
|
11258
|
+
* `exited` is raced deliberately. Without it, a shell that dies reports as
|
|
11259
|
+
* "timed out waiting for X" — sending whoever reads it to lengthen a timeout
|
|
11260
|
+
* for a process that is not running. Naming the exit is the difference
|
|
11261
|
+
* between a diagnosis and a wrong lead.
|
|
11262
|
+
*/
|
|
11263
|
+
waitFor(pattern, options) {
|
|
11264
|
+
return new Promise((resolve2) => {
|
|
11265
|
+
let settled = false;
|
|
11266
|
+
let timer2;
|
|
11267
|
+
const finish = (result) => {
|
|
11268
|
+
if (settled) return;
|
|
11269
|
+
settled = true;
|
|
11270
|
+
this.waiters.delete(check);
|
|
11271
|
+
if (timer2) clearTimeout(timer2);
|
|
11272
|
+
resolve2(result);
|
|
11273
|
+
};
|
|
11274
|
+
const check = () => {
|
|
11275
|
+
pattern.lastIndex = 0;
|
|
11276
|
+
const match = pattern.exec(this.text);
|
|
11277
|
+
if (!match) return;
|
|
11278
|
+
const through = match.index + (match[0].length || 1);
|
|
11279
|
+
const text3 = this.text.slice(0, through);
|
|
11280
|
+
this.text = this.text.slice(through);
|
|
11281
|
+
finish({ status: "matched", text: text3, match });
|
|
11282
|
+
};
|
|
11283
|
+
this.waiters.add(check);
|
|
11284
|
+
check();
|
|
11285
|
+
if (settled) return;
|
|
11286
|
+
if (options.timeoutMs > 0) {
|
|
11287
|
+
timer2 = setTimeout(() => {
|
|
11288
|
+
const text3 = this.text;
|
|
11289
|
+
this.text = "";
|
|
11290
|
+
finish({ status: "timeout", text: text3 });
|
|
11291
|
+
}, options.timeoutMs);
|
|
11292
|
+
timer2.unref?.();
|
|
11293
|
+
}
|
|
11294
|
+
options.exited?.then(
|
|
11295
|
+
(exit) => {
|
|
11296
|
+
if (settled) return;
|
|
11297
|
+
const text3 = this.text;
|
|
11298
|
+
this.text = "";
|
|
11299
|
+
finish({ status: "exited", text: text3, exitCode: exit.exitCode, signal: exit.signal });
|
|
11300
|
+
},
|
|
11301
|
+
() => {
|
|
11302
|
+
}
|
|
11303
|
+
);
|
|
11304
|
+
});
|
|
11305
|
+
}
|
|
11306
|
+
};
|
|
11307
|
+
|
|
11308
|
+
// src/runtime/terminal-sessions.ts
|
|
11309
|
+
var TerminalSessions = class {
|
|
11310
|
+
constructor(graph) {
|
|
11311
|
+
this.graph = graph;
|
|
11312
|
+
this.open = /* @__PURE__ */ new Map();
|
|
11313
|
+
this.laneOf = /* @__PURE__ */ new Map();
|
|
11314
|
+
this.transcripts = /* @__PURE__ */ new Map();
|
|
11315
|
+
this.listening = /* @__PURE__ */ new Map();
|
|
11316
|
+
}
|
|
11317
|
+
/**
|
|
11318
|
+
* The terminal lane a node belongs to, or null.
|
|
11319
|
+
*
|
|
11320
|
+
* Walks up `parentId` rather than checking only the immediate parent, and
|
|
11321
|
+
* caches per node id — a graph is walked once per run, and the answer cannot
|
|
11322
|
+
* change while it runs.
|
|
11323
|
+
*/
|
|
11324
|
+
laneFor(nodeId, isTerminalLane) {
|
|
11325
|
+
const cached = this.laneOf.get(nodeId);
|
|
11326
|
+
if (cached !== void 0) return cached;
|
|
11327
|
+
const byId = new Map(this.graph.nodes.map((n) => [n.id, n]));
|
|
11328
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11329
|
+
let current = byId.get(nodeId);
|
|
11330
|
+
let answer = null;
|
|
11331
|
+
while (current) {
|
|
11332
|
+
if (seen.has(current.id)) break;
|
|
11333
|
+
seen.add(current.id);
|
|
11334
|
+
if (current.id !== nodeId && isTerminalLane(current)) {
|
|
11335
|
+
answer = current.id;
|
|
11336
|
+
break;
|
|
11337
|
+
}
|
|
11338
|
+
const parentId = current.parentId;
|
|
11339
|
+
current = parentId ? byId.get(parentId) : void 0;
|
|
11340
|
+
}
|
|
11341
|
+
this.laneOf.set(nodeId, answer);
|
|
11342
|
+
return answer;
|
|
11343
|
+
}
|
|
11344
|
+
/**
|
|
11345
|
+
* The session for a lane, opening it on first use.
|
|
11346
|
+
*
|
|
11347
|
+
* The PROMISE is cached, not the resolved session. Two nodes cannot run
|
|
11348
|
+
* concurrently in the current engine, but a caller that awaits a
|
|
11349
|
+
* half-finished open would otherwise start a second process — and two shells
|
|
11350
|
+
* where a graph says one is the failure this class exists to prevent, arriving
|
|
11351
|
+
* only under concurrency and therefore only sometimes.
|
|
11352
|
+
*/
|
|
11353
|
+
session(laneId, spec) {
|
|
11354
|
+
const existing = this.open.get(laneId);
|
|
11355
|
+
if (existing) return existing;
|
|
11356
|
+
{
|
|
11357
|
+
const failed = Promise.reject(
|
|
11358
|
+
new Error(
|
|
11359
|
+
"No terminal host is registered. A terminal lane needs one \u2014 call registerTerminalHost() from the desktop app, or install @particle-academy/fancy-flow/terminal/fancy-term-host."
|
|
11360
|
+
)
|
|
11361
|
+
);
|
|
11362
|
+
this.open.set(laneId, failed);
|
|
11363
|
+
failed.catch(() => {
|
|
11364
|
+
});
|
|
11365
|
+
return failed;
|
|
11366
|
+
}
|
|
11367
|
+
}
|
|
11368
|
+
/**
|
|
11369
|
+
* The accumulated output for a lane.
|
|
11370
|
+
*
|
|
11371
|
+
* Created on demand and kept for the run, so it exists before the session
|
|
11372
|
+
* resolves and survives every node that reads it. Consuming is the reader's
|
|
11373
|
+
* job — see `TerminalTranscript.waitFor`.
|
|
11374
|
+
*/
|
|
11375
|
+
transcriptFor(laneId) {
|
|
11376
|
+
const existing = this.transcripts.get(laneId);
|
|
11377
|
+
if (existing) return existing;
|
|
11378
|
+
const created = new TerminalTranscript();
|
|
11379
|
+
this.transcripts.set(laneId, created);
|
|
11380
|
+
return created;
|
|
11381
|
+
}
|
|
11382
|
+
/** True once a lane has a session — used to avoid opening one during teardown. */
|
|
11383
|
+
isOpen(laneId) {
|
|
11384
|
+
return this.open.has(laneId);
|
|
11385
|
+
}
|
|
11386
|
+
/**
|
|
11387
|
+
* Close every session this run opened.
|
|
11388
|
+
*
|
|
11389
|
+
* Every close is attempted even if one throws: a host that fails to close one
|
|
11390
|
+
* PTY must not strand the others, which would leave processes alive after the
|
|
11391
|
+
* run reported that it had finished. Errors are returned rather than thrown,
|
|
11392
|
+
* because teardown runs in a `finally` and throwing there would replace the
|
|
11393
|
+
* run's real error with a cleanup error.
|
|
11394
|
+
*/
|
|
11395
|
+
async closeAll() {
|
|
11396
|
+
const errors = [];
|
|
11397
|
+
for (const [laneId, pending] of this.open) {
|
|
11398
|
+
try {
|
|
11399
|
+
this.listening.get(laneId)?.();
|
|
11400
|
+
this.listening.delete(laneId);
|
|
11401
|
+
const session = await pending;
|
|
11402
|
+
await session.close();
|
|
11403
|
+
} catch (e) {
|
|
11404
|
+
errors.push(e instanceof Error ? e : new Error(`${laneId}: ${String(e)}`));
|
|
11405
|
+
}
|
|
11406
|
+
}
|
|
11407
|
+
this.open.clear();
|
|
11408
|
+
this.listening.clear();
|
|
11409
|
+
this.transcripts.clear();
|
|
11410
|
+
return errors;
|
|
11411
|
+
}
|
|
11412
|
+
};
|
|
11413
|
+
function specForLane(lane) {
|
|
11414
|
+
const config = lane.data?.config ?? {};
|
|
11415
|
+
const text3 = (key) => {
|
|
11416
|
+
const value = config[key];
|
|
11417
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
11418
|
+
};
|
|
11419
|
+
const env = config.env;
|
|
11420
|
+
return {
|
|
11421
|
+
command: text3("command"),
|
|
11422
|
+
cwd: text3("cwd"),
|
|
11423
|
+
env: env && typeof env === "object" && !Array.isArray(env) ? Object.fromEntries(
|
|
11424
|
+
Object.entries(env).map(([k2, v2]) => [k2, String(v2)])
|
|
11425
|
+
) : void 0
|
|
11426
|
+
};
|
|
11427
|
+
}
|
|
11428
|
+
|
|
11196
11429
|
// src/runtime/workflow-props.ts
|
|
11197
11430
|
function typeOf(value) {
|
|
11198
11431
|
if (value === null) return "null";
|
|
@@ -11271,6 +11504,25 @@ async function runFlow(graph, executors, onEvent = () => {
|
|
|
11271
11504
|
const props = propsCheck.props;
|
|
11272
11505
|
const declaresProps = (graph.inputs?.length ?? 0) > 0;
|
|
11273
11506
|
const incomingByNode = indexIncoming(graph.edges);
|
|
11507
|
+
const sessions = new TerminalSessions(graph);
|
|
11508
|
+
const isTerminalLane = (candidate) => getNodeKind(candidate.type ?? "")?.name === "@particle-academy/terminal_lane";
|
|
11509
|
+
const terminalAccessorFor = (node) => {
|
|
11510
|
+
const laneId = sessions.laneFor(node.id, isTerminalLane);
|
|
11511
|
+
if (laneId === null) return void 0;
|
|
11512
|
+
const lane = graph.nodes.find((n) => n.id === laneId);
|
|
11513
|
+
if (!lane) return void 0;
|
|
11514
|
+
return {
|
|
11515
|
+
session: () => sessions.session(laneId, specForLane(lane)),
|
|
11516
|
+
// Awaits the session first on purpose. A transcript handed out before
|
|
11517
|
+
// anything is feeding it would look perfectly healthy and match nothing,
|
|
11518
|
+
// and "waited and nothing came" is the hardest failure here to tell apart
|
|
11519
|
+
// from a process that is simply slow.
|
|
11520
|
+
transcript: async () => {
|
|
11521
|
+
await sessions.session(laneId, specForLane(lane));
|
|
11522
|
+
return sessions.transcriptFor(laneId);
|
|
11523
|
+
}
|
|
11524
|
+
};
|
|
11525
|
+
};
|
|
11274
11526
|
const timer2 = timeoutMs ? setTimeout(() => errors.push(`Run timed out after ${timeoutMs}ms`), timeoutMs) : null;
|
|
11275
11527
|
onEvent({ type: "run-start" });
|
|
11276
11528
|
try {
|
|
@@ -11336,7 +11588,8 @@ async function runFlow(graph, executors, onEvent = () => {
|
|
|
11336
11588
|
emit: onEvent,
|
|
11337
11589
|
executors,
|
|
11338
11590
|
depth,
|
|
11339
|
-
run
|
|
11591
|
+
run,
|
|
11592
|
+
terminal: terminalAccessorFor(node)
|
|
11340
11593
|
})
|
|
11341
11594
|
);
|
|
11342
11595
|
outputs[node.id] = result;
|
|
@@ -11358,6 +11611,9 @@ async function runFlow(graph, executors, onEvent = () => {
|
|
|
11358
11611
|
}
|
|
11359
11612
|
} finally {
|
|
11360
11613
|
if (timer2) clearTimeout(timer2);
|
|
11614
|
+
for (const error of await sessions.closeAll()) {
|
|
11615
|
+
onEvent({ type: "log", level: "warn", message: `terminal close failed: ${error.message}` });
|
|
11616
|
+
}
|
|
11361
11617
|
}
|
|
11362
11618
|
const ok = errors.length === 0;
|
|
11363
11619
|
onEvent({ type: "run-end", ok });
|
|
@@ -11436,7 +11692,7 @@ function pickExecutor(executors, node) {
|
|
|
11436
11692
|
for (const id2 of executorLookupIds(node)) {
|
|
11437
11693
|
if (executors[id2]) return executors[id2];
|
|
11438
11694
|
}
|
|
11439
|
-
return
|
|
11695
|
+
return getNodeKind(node.type ?? "")?.executor;
|
|
11440
11696
|
}
|
|
11441
11697
|
function activatedPorts(node, result) {
|
|
11442
11698
|
if (result && typeof result === "object") {
|
|
@@ -11546,6 +11802,376 @@ var subflowExecutor = async (ctx) => {
|
|
|
11546
11802
|
return { __port: "out", value: result.outputs };
|
|
11547
11803
|
};
|
|
11548
11804
|
|
|
11805
|
+
// src/expressions/expr.ts
|
|
11806
|
+
function wholeExpression(trimmed) {
|
|
11807
|
+
if (trimmed.length < 4) return null;
|
|
11808
|
+
if (!trimmed.startsWith("{{") || !trimmed.endsWith("}}")) return null;
|
|
11809
|
+
return trimmed.slice(2, -2);
|
|
11810
|
+
}
|
|
11811
|
+
function interpolate(template, resolve2) {
|
|
11812
|
+
let out = "";
|
|
11813
|
+
let i = 0;
|
|
11814
|
+
for (; ; ) {
|
|
11815
|
+
const open = template.indexOf("{{", i);
|
|
11816
|
+
if (open === -1) return out + template.slice(i);
|
|
11817
|
+
const close = template.indexOf("}}", open + 2);
|
|
11818
|
+
if (close === -1) return out + template.slice(i);
|
|
11819
|
+
out += template.slice(i, open) + resolve2(template.slice(open + 2, close));
|
|
11820
|
+
i = close + 2;
|
|
11821
|
+
}
|
|
11822
|
+
}
|
|
11823
|
+
var FALSY_STRINGS = /* @__PURE__ */ new Set(["", "0", "false", "no", "off", "null"]);
|
|
11824
|
+
var UnresolvedPathError = class extends Error {
|
|
11825
|
+
constructor(path) {
|
|
11826
|
+
super(
|
|
11827
|
+
`Expression path "${path}" did not resolve. Under the "throw" policy an unresolvable path is an error rather than an empty string.`
|
|
11828
|
+
);
|
|
11829
|
+
this.path = path;
|
|
11830
|
+
this.name = "UnresolvedPathError";
|
|
11831
|
+
}
|
|
11832
|
+
};
|
|
11833
|
+
function tryResolvePath(path, context) {
|
|
11834
|
+
const unresolved = { resolved: false, value: null };
|
|
11835
|
+
const trimmed = path.trim();
|
|
11836
|
+
if (trimmed === "") return unresolved;
|
|
11837
|
+
const segments = trimmed.split(".");
|
|
11838
|
+
let cursor;
|
|
11839
|
+
const head = segments[0];
|
|
11840
|
+
if (head === "$json" || head === "$input") {
|
|
11841
|
+
cursor = context !== null && typeof context === "object" && "in" in context ? context.in : context;
|
|
11842
|
+
segments.shift();
|
|
11843
|
+
} else {
|
|
11844
|
+
cursor = context;
|
|
11845
|
+
}
|
|
11846
|
+
for (const segment of segments) {
|
|
11847
|
+
if (cursor === null || cursor === void 0) return unresolved;
|
|
11848
|
+
if (typeof cursor !== "object") return unresolved;
|
|
11849
|
+
const next = cursor[segment];
|
|
11850
|
+
if (next === void 0) return unresolved;
|
|
11851
|
+
cursor = next;
|
|
11852
|
+
}
|
|
11853
|
+
return { resolved: true, value: cursor === void 0 ? null : cursor };
|
|
11854
|
+
}
|
|
11855
|
+
function resolvePath(path, context) {
|
|
11856
|
+
return tryResolvePath(path, context).value;
|
|
11857
|
+
}
|
|
11858
|
+
function evaluateExpression(template, context, options = {}) {
|
|
11859
|
+
if (typeof template !== "string") return template;
|
|
11860
|
+
const policy = options.onUnresolved ?? "empty";
|
|
11861
|
+
const whole = wholeExpression(template.trim());
|
|
11862
|
+
if (whole !== null) {
|
|
11863
|
+
const r = tryResolvePath(whole, context);
|
|
11864
|
+
if (r.resolved) return r.value;
|
|
11865
|
+
if (policy === "throw") throw new UnresolvedPathError(whole);
|
|
11866
|
+
return policy === "keep" ? template : null;
|
|
11867
|
+
}
|
|
11868
|
+
return interpolate(template, (path) => {
|
|
11869
|
+
const r = tryResolvePath(path, context);
|
|
11870
|
+
if (r.resolved) return stringify(r.value);
|
|
11871
|
+
if (policy === "throw") throw new UnresolvedPathError(path);
|
|
11872
|
+
return policy === "keep" ? `{{${path}}}` : "";
|
|
11873
|
+
});
|
|
11874
|
+
}
|
|
11875
|
+
function truthy(value) {
|
|
11876
|
+
if (typeof value === "boolean") return value;
|
|
11877
|
+
if (value === null || value === void 0) return false;
|
|
11878
|
+
if (typeof value === "string") return !FALSY_STRINGS.has(value.trim().toLowerCase());
|
|
11879
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
11880
|
+
if (typeof value === "number") return value !== 0;
|
|
11881
|
+
return Boolean(value);
|
|
11882
|
+
}
|
|
11883
|
+
function text(value) {
|
|
11884
|
+
return stringify(value);
|
|
11885
|
+
}
|
|
11886
|
+
function stringify(value) {
|
|
11887
|
+
if (typeof value === "string") return value;
|
|
11888
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
11889
|
+
if (value === null || value === void 0) return "";
|
|
11890
|
+
if (typeof value === "number" || typeof value === "bigint") return String(value);
|
|
11891
|
+
try {
|
|
11892
|
+
return JSON.stringify(value) ?? "";
|
|
11893
|
+
} catch {
|
|
11894
|
+
return "";
|
|
11895
|
+
}
|
|
11896
|
+
}
|
|
11897
|
+
function evaluateConfig(config, context, options = {}) {
|
|
11898
|
+
const out = {};
|
|
11899
|
+
for (const [key, value] of Object.entries(config)) {
|
|
11900
|
+
out[key] = Array.isArray(value) ? value.map((v2) => evaluateExpression(v2, context, options)) : value !== null && typeof value === "object" ? evaluateConfig(value, context, options) : evaluateExpression(value, context, options);
|
|
11901
|
+
}
|
|
11902
|
+
return out;
|
|
11903
|
+
}
|
|
11904
|
+
|
|
11905
|
+
// src/registry/logic.ts
|
|
11906
|
+
function configOf(node) {
|
|
11907
|
+
return node?.data?.config ?? {};
|
|
11908
|
+
}
|
|
11909
|
+
function contextOf(inputs) {
|
|
11910
|
+
return inputs;
|
|
11911
|
+
}
|
|
11912
|
+
function resolve(value, inputs) {
|
|
11913
|
+
if (typeof value !== "string") return value;
|
|
11914
|
+
return evaluateExpression(value, contextOf(inputs));
|
|
11915
|
+
}
|
|
11916
|
+
function conditionHolds(row, inputs) {
|
|
11917
|
+
const left = resolve(row.left, inputs);
|
|
11918
|
+
const right = resolve(row.right, inputs);
|
|
11919
|
+
const operator = typeof row.operator === "string" ? row.operator : "eq";
|
|
11920
|
+
const asText = (v2) => text(v2);
|
|
11921
|
+
const asNumber = (v2) => {
|
|
11922
|
+
const n = typeof v2 === "number" ? v2 : Number(asText(v2));
|
|
11923
|
+
return Number.isFinite(n) ? n : NaN;
|
|
11924
|
+
};
|
|
11925
|
+
const numeric = (fn2) => {
|
|
11926
|
+
const a = asNumber(left);
|
|
11927
|
+
const b = asNumber(right);
|
|
11928
|
+
return Number.isFinite(a) && Number.isFinite(b) ? fn2(a, b) : false;
|
|
11929
|
+
};
|
|
11930
|
+
const isEmpty = (v2) => v2 === null || v2 === void 0 || v2 === "" || Array.isArray(v2) && v2.length === 0 || typeof v2 === "object" && !Array.isArray(v2) && Object.keys(v2).length === 0;
|
|
11931
|
+
switch (operator) {
|
|
11932
|
+
// Compared as TEXT, deliberately. `right` is a text field in the schema, so
|
|
11933
|
+
// `status == "2"` must match the number 2 an upstream node produced —
|
|
11934
|
+
// strict equality there would fail on a difference the author cannot see.
|
|
11935
|
+
case "eq":
|
|
11936
|
+
return asText(left) === asText(right);
|
|
11937
|
+
case "neq":
|
|
11938
|
+
return asText(left) !== asText(right);
|
|
11939
|
+
case "contains":
|
|
11940
|
+
return asText(left).includes(asText(right));
|
|
11941
|
+
case "not_contains":
|
|
11942
|
+
return !asText(left).includes(asText(right));
|
|
11943
|
+
case "gt":
|
|
11944
|
+
return numeric((a, b) => a > b);
|
|
11945
|
+
case "gte":
|
|
11946
|
+
return numeric((a, b) => a >= b);
|
|
11947
|
+
case "lt":
|
|
11948
|
+
return numeric((a, b) => a < b);
|
|
11949
|
+
case "lte":
|
|
11950
|
+
return numeric((a, b) => a <= b);
|
|
11951
|
+
case "truthy":
|
|
11952
|
+
return truthy(left);
|
|
11953
|
+
case "falsy":
|
|
11954
|
+
return !truthy(left);
|
|
11955
|
+
case "empty":
|
|
11956
|
+
return isEmpty(left);
|
|
11957
|
+
case "not_empty":
|
|
11958
|
+
return !isEmpty(left);
|
|
11959
|
+
default:
|
|
11960
|
+
return false;
|
|
11961
|
+
}
|
|
11962
|
+
}
|
|
11963
|
+
var branchExecutor = (ctx) => {
|
|
11964
|
+
const config = configOf(ctx.node);
|
|
11965
|
+
const inputs = ctx.inputs;
|
|
11966
|
+
let taken;
|
|
11967
|
+
const raw = config.condition;
|
|
11968
|
+
if (typeof raw === "string" && raw.trim() !== "") {
|
|
11969
|
+
taken = truthy(resolve(raw, inputs));
|
|
11970
|
+
} else {
|
|
11971
|
+
const rows = Array.isArray(config.conditions) ? config.conditions : [];
|
|
11972
|
+
if (rows.length === 0) {
|
|
11973
|
+
taken = false;
|
|
11974
|
+
} else {
|
|
11975
|
+
const results = rows.map((row) => conditionHolds(row, inputs));
|
|
11976
|
+
taken = config.match === "any" ? results.some(Boolean) : results.every(Boolean);
|
|
11977
|
+
}
|
|
11978
|
+
}
|
|
11979
|
+
return { __port: taken ? "true" : "false", value: ctx.inputs.in ?? ctx.inputs };
|
|
11980
|
+
};
|
|
11981
|
+
var transformExecutor = (ctx) => {
|
|
11982
|
+
const config = configOf(ctx.node);
|
|
11983
|
+
const inputs = ctx.inputs;
|
|
11984
|
+
const passthrough = ctx.inputs.in ?? ctx.inputs;
|
|
11985
|
+
if (config.mode === "expression") {
|
|
11986
|
+
const expression = config.expression;
|
|
11987
|
+
if (typeof expression !== "string" || expression.trim() === "") return passthrough;
|
|
11988
|
+
return resolve(expression, inputs);
|
|
11989
|
+
}
|
|
11990
|
+
const rows = Array.isArray(config.fields) ? config.fields : [];
|
|
11991
|
+
if (rows.length === 0) return passthrough;
|
|
11992
|
+
const out = {};
|
|
11993
|
+
let wrote = false;
|
|
11994
|
+
for (const row of rows) {
|
|
11995
|
+
const key = typeof row.key === "string" ? row.key.trim() : "";
|
|
11996
|
+
if (key === "") continue;
|
|
11997
|
+
out[key] = resolve(row.value, inputs);
|
|
11998
|
+
wrote = true;
|
|
11999
|
+
}
|
|
12000
|
+
return wrote ? out : passthrough;
|
|
12001
|
+
};
|
|
12002
|
+
var mergeExecutor = (ctx) => {
|
|
12003
|
+
const entries = Object.entries(ctx.inputs);
|
|
12004
|
+
if (configOf(ctx.node).mode === "concat") {
|
|
12005
|
+
const out = [];
|
|
12006
|
+
for (const [, value] of entries) {
|
|
12007
|
+
if (value === null || value === void 0) continue;
|
|
12008
|
+
if (Array.isArray(value)) out.push(...value);
|
|
12009
|
+
else out.push(value);
|
|
12010
|
+
}
|
|
12011
|
+
return out;
|
|
12012
|
+
}
|
|
12013
|
+
const merged = {};
|
|
12014
|
+
for (const [port, value] of entries) {
|
|
12015
|
+
if (value === null || value === void 0) continue;
|
|
12016
|
+
if (typeof value === "object" && !Array.isArray(value)) Object.assign(merged, value);
|
|
12017
|
+
else merged[port] = value;
|
|
12018
|
+
}
|
|
12019
|
+
return merged;
|
|
12020
|
+
};
|
|
12021
|
+
var forEachExecutor = (ctx) => {
|
|
12022
|
+
const source = resolve(configOf(ctx.node).source, ctx.inputs);
|
|
12023
|
+
let items;
|
|
12024
|
+
if (Array.isArray(source)) items = source;
|
|
12025
|
+
else if (source === null || source === void 0) items = [];
|
|
12026
|
+
else if (typeof source === "object") items = Object.values(source);
|
|
12027
|
+
else items = [source];
|
|
12028
|
+
return { items, count: items.length };
|
|
12029
|
+
};
|
|
12030
|
+
|
|
12031
|
+
// src/registry/terminal-nodes.ts
|
|
12032
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
12033
|
+
var ENTER = "\r";
|
|
12034
|
+
function configOf2(node) {
|
|
12035
|
+
return node?.data?.config ?? {};
|
|
12036
|
+
}
|
|
12037
|
+
function text2(config, key) {
|
|
12038
|
+
const value = config[key];
|
|
12039
|
+
return typeof value === "string" ? value : "";
|
|
12040
|
+
}
|
|
12041
|
+
function millis(config, key, fallback) {
|
|
12042
|
+
const value = Number(config[key]);
|
|
12043
|
+
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
12044
|
+
}
|
|
12045
|
+
function exitMarker(nonce) {
|
|
12046
|
+
const token = `__fancy_flow_exit_${nonce}__`;
|
|
12047
|
+
return {
|
|
12048
|
+
token,
|
|
12049
|
+
// The expanded status is required to be digits. The shell ECHOES the typed
|
|
12050
|
+
// command back first, and that echo contains the marker with `$?`
|
|
12051
|
+
// unexpanded — so requiring digits is what stops the node matching its own
|
|
12052
|
+
// command line and reporting success before anything has run.
|
|
12053
|
+
pattern: new RegExp(`${token}:(\\d+)`)
|
|
12054
|
+
};
|
|
12055
|
+
}
|
|
12056
|
+
var counter = 0;
|
|
12057
|
+
function newNonce() {
|
|
12058
|
+
counter += 1;
|
|
12059
|
+
return `${Date.now().toString(36)}${counter.toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
12060
|
+
}
|
|
12061
|
+
function withoutMarker(output, token) {
|
|
12062
|
+
return output.split("\n").filter((line) => !line.includes(token)).join("\n").trim();
|
|
12063
|
+
}
|
|
12064
|
+
function fail(ctx, message) {
|
|
12065
|
+
ctx.abort(message);
|
|
12066
|
+
throw new Error(message);
|
|
12067
|
+
}
|
|
12068
|
+
function requireTerminal(ctx) {
|
|
12069
|
+
if (!ctx.terminal) {
|
|
12070
|
+
fail(
|
|
12071
|
+
ctx,
|
|
12072
|
+
`"${String(ctx.node.data?.label ?? ctx.node.id)}" is a terminal node but is not inside a terminal lane. Drag it into one \u2014 a terminal node outside a lane has no session to talk to, and opening a private shell for it would defeat the point of the lane.`
|
|
12073
|
+
);
|
|
12074
|
+
}
|
|
12075
|
+
return ctx.terminal;
|
|
12076
|
+
}
|
|
12077
|
+
function compilePattern(ctx, raw, config) {
|
|
12078
|
+
const flags = typeof config.flags === "string" ? config.flags.replace(/[gy]/g, "") : "";
|
|
12079
|
+
try {
|
|
12080
|
+
return config.mode === "regex" ? new RegExp(raw, flags) : new RegExp(raw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), flags);
|
|
12081
|
+
} catch (e) {
|
|
12082
|
+
return fail(ctx, `terminal_await has an invalid regex: ${e instanceof Error ? e.message : String(e)}`);
|
|
12083
|
+
}
|
|
12084
|
+
}
|
|
12085
|
+
var terminalRunExecutor = async (ctx) => {
|
|
12086
|
+
const config = configOf2(ctx.node);
|
|
12087
|
+
const command = text2(config, "command").trim();
|
|
12088
|
+
if (!command) fail(ctx, "terminal_run has no command configured");
|
|
12089
|
+
const terminal = requireTerminal(ctx);
|
|
12090
|
+
const session = await terminal.session();
|
|
12091
|
+
const transcript = await terminal.transcript();
|
|
12092
|
+
const { token, pattern } = exitMarker(newNonce());
|
|
12093
|
+
const timeoutMs = millis(config, "timeoutMs", DEFAULT_TIMEOUT_MS);
|
|
12094
|
+
transcript.clear();
|
|
12095
|
+
await session.write(`${command}; printf '${token}:%s\\n' "$?"${ENTER}`);
|
|
12096
|
+
const result = await transcript.waitFor(pattern, { timeoutMs, exited: session.exited });
|
|
12097
|
+
if (result.status === "exited") {
|
|
12098
|
+
fail(
|
|
12099
|
+
ctx,
|
|
12100
|
+
`The terminal exited (code ${result.exitCode}${result.signal ? `, signal ${result.signal}` : ""}) while running "${command}". The lane's session is gone, so every later node in this lane would fail too.`
|
|
12101
|
+
);
|
|
12102
|
+
}
|
|
12103
|
+
if (result.status === "timeout") {
|
|
12104
|
+
fail(
|
|
12105
|
+
ctx,
|
|
12106
|
+
`"${command}" did not finish within ${timeoutMs}ms. Raise the node's timeout if it is genuinely slow \u2014 but a command that waits for input never finishes at all, and needs terminal_send + terminal_await.`
|
|
12107
|
+
);
|
|
12108
|
+
}
|
|
12109
|
+
const exitCode = Number(result.match[1]);
|
|
12110
|
+
const output = withoutMarker(result.text, token);
|
|
12111
|
+
const failOnNonZero = config.failOnNonZero !== false;
|
|
12112
|
+
if (failOnNonZero && exitCode !== 0) {
|
|
12113
|
+
ctx.abort(`"${command}" exited ${exitCode}.
|
|
12114
|
+
${output}`);
|
|
12115
|
+
}
|
|
12116
|
+
return { output, exitCode, command };
|
|
12117
|
+
};
|
|
12118
|
+
var terminalSendExecutor = async (ctx) => {
|
|
12119
|
+
const config = configOf2(ctx.node);
|
|
12120
|
+
const terminal = requireTerminal(ctx);
|
|
12121
|
+
const session = await terminal.session();
|
|
12122
|
+
const body = text2(config, "text");
|
|
12123
|
+
const submit = config.submit !== false;
|
|
12124
|
+
if (config.clearFirst === true) {
|
|
12125
|
+
(await terminal.transcript()).clear();
|
|
12126
|
+
}
|
|
12127
|
+
await session.write(submit ? `${body}${ENTER}` : body);
|
|
12128
|
+
return { sent: body, submitted: submit };
|
|
12129
|
+
};
|
|
12130
|
+
var terminalAwaitExecutor = async (ctx) => {
|
|
12131
|
+
const config = configOf2(ctx.node);
|
|
12132
|
+
const terminal = requireTerminal(ctx);
|
|
12133
|
+
const session = await terminal.session();
|
|
12134
|
+
const transcript = await terminal.transcript();
|
|
12135
|
+
const raw = text2(config, "pattern").trim();
|
|
12136
|
+
if (!raw) fail(ctx, "terminal_await has no pattern configured \u2014 there is nothing for it to wait for");
|
|
12137
|
+
const pattern = compilePattern(ctx, raw, config);
|
|
12138
|
+
const timeoutMs = millis(config, "timeoutMs", DEFAULT_TIMEOUT_MS);
|
|
12139
|
+
const result = await transcript.waitFor(pattern, {
|
|
12140
|
+
timeoutMs,
|
|
12141
|
+
exited: session.exited
|
|
12142
|
+
});
|
|
12143
|
+
if (result.status === "exited") {
|
|
12144
|
+
fail(
|
|
12145
|
+
ctx,
|
|
12146
|
+
`The terminal exited (code ${result.exitCode}${result.signal ? `, signal ${result.signal}` : ""}) while waiting for ${JSON.stringify(raw)}. It never appeared, and the lane's session is gone.`
|
|
12147
|
+
);
|
|
12148
|
+
}
|
|
12149
|
+
if (result.status === "timeout") {
|
|
12150
|
+
if (config.onTimeout === "continue") {
|
|
12151
|
+
ctx.emit({
|
|
12152
|
+
type: "log",
|
|
12153
|
+
nodeId: ctx.node.id,
|
|
12154
|
+
level: "warn",
|
|
12155
|
+
message: `terminal_await: ${JSON.stringify(raw)} did not appear within ${timeoutMs}ms; continuing as configured.`
|
|
12156
|
+
});
|
|
12157
|
+
return { matched: false, output: result.text, groups: [] };
|
|
12158
|
+
}
|
|
12159
|
+
fail(
|
|
12160
|
+
ctx,
|
|
12161
|
+
`${JSON.stringify(raw)} did not appear within ${timeoutMs}ms. Last output:
|
|
12162
|
+
${result.text.slice(-2e3)}`
|
|
12163
|
+
);
|
|
12164
|
+
}
|
|
12165
|
+
return {
|
|
12166
|
+
matched: true,
|
|
12167
|
+
output: result.text,
|
|
12168
|
+
matchedText: result.match[0],
|
|
12169
|
+
// Capture groups are the reason to use regex mode at all — a prompt that
|
|
12170
|
+
// reports a session id or a file path is only useful if the value comes out.
|
|
12171
|
+
groups: result.match.slice(1)
|
|
12172
|
+
};
|
|
12173
|
+
};
|
|
12174
|
+
|
|
11549
12175
|
// src/registry/builtin.ts
|
|
11550
12176
|
var HUMAN_FIELD_OUTPUT_TYPE = {
|
|
11551
12177
|
text: "string",
|
|
@@ -11835,6 +12461,9 @@ var KINDS = [
|
|
|
11835
12461
|
// returns its input on the chosen port
|
|
11836
12462
|
emits: "input",
|
|
11837
12463
|
aliases: ["branch", "@fancy/branch"],
|
|
12464
|
+
// Pure logic: no I/O to choose, so a default is safe. A host
|
|
12465
|
+
// executor still wins -- pickExecutor consults the registry first.
|
|
12466
|
+
executor: branchExecutor,
|
|
11838
12467
|
category: "logic",
|
|
11839
12468
|
label: "Branch",
|
|
11840
12469
|
description: "Multi-way branch on a condition or value.",
|
|
@@ -11932,6 +12561,9 @@ var KINDS = [
|
|
|
11932
12561
|
{ path: "count", type: "number" }
|
|
11933
12562
|
],
|
|
11934
12563
|
aliases: ["for_each", "@fancy/for_each"],
|
|
12564
|
+
// Pure logic: no I/O to choose, so a default is safe. A host
|
|
12565
|
+
// executor still wins -- pickExecutor consults the registry first.
|
|
12566
|
+
executor: forEachExecutor,
|
|
11935
12567
|
category: "logic",
|
|
11936
12568
|
label: "For Each",
|
|
11937
12569
|
description: "Iterate over a list, emitting each item on `item`.",
|
|
@@ -11952,6 +12584,9 @@ var KINDS = [
|
|
|
11952
12584
|
// reference.
|
|
11953
12585
|
emits: (config) => (config?.mode ?? "merge") === "concat" ? null : "inputs-merged",
|
|
11954
12586
|
aliases: ["merge", "@fancy/merge"],
|
|
12587
|
+
// Pure logic: no I/O to choose, so a default is safe. A host
|
|
12588
|
+
// executor still wins -- pickExecutor consults the registry first.
|
|
12589
|
+
executor: mergeExecutor,
|
|
11955
12590
|
category: "logic",
|
|
11956
12591
|
label: "Merge",
|
|
11957
12592
|
description: "Combine multiple inputs into one object or array.",
|
|
@@ -11997,6 +12632,9 @@ var KINDS = [
|
|
|
11997
12632
|
// else the shape that expression names. So the RELATION is config-dependent.
|
|
11998
12633
|
emits: (config) => (config?.expression ?? "") === "" ? "input" : "expression:expression",
|
|
11999
12634
|
aliases: ["transform", "@fancy/transform"],
|
|
12635
|
+
// Pure logic: no I/O to choose, so a default is safe. A host
|
|
12636
|
+
// executor still wins -- pickExecutor consults the registry first.
|
|
12637
|
+
executor: transformExecutor,
|
|
12000
12638
|
category: "logic",
|
|
12001
12639
|
label: "Transform",
|
|
12002
12640
|
description: "Reshape data with an expression.",
|
|
@@ -12455,6 +13093,179 @@ var KINDS = [
|
|
|
12455
13093
|
resizable: { minWidth: 160, minHeight: 72 },
|
|
12456
13094
|
component: LaneNode
|
|
12457
13095
|
},
|
|
13096
|
+
{
|
|
13097
|
+
// Terminal lane — a lane that OWNS a terminal for the length of the run.
|
|
13098
|
+
//
|
|
13099
|
+
// Visually a swimlane, and `layout` for the same reason the plain lane is:
|
|
13100
|
+
// a lane is not a step, so it never executes and never appears in topo
|
|
13101
|
+
// order. What is new is that it DECLARES a resource. The runtime opens its
|
|
13102
|
+
// terminal lazily, when the first terminal node inside it runs, and closes
|
|
13103
|
+
// it in the run's `finally` — so a graph that never reaches a terminal node
|
|
13104
|
+
// never spawns a process, and one that does gets exactly one session no
|
|
13105
|
+
// matter how many nodes use it.
|
|
13106
|
+
//
|
|
13107
|
+
// Membership is `parentId`, which already persists into the WorkflowSchema.
|
|
13108
|
+
// That is what lets a headless runtime resolve the same grouping the canvas
|
|
13109
|
+
// shows, without a second association to keep in sync.
|
|
13110
|
+
name: "@particle-academy/terminal_lane",
|
|
13111
|
+
aliases: ["terminal_lane", "@fancy/terminal_lane"],
|
|
13112
|
+
category: "layout",
|
|
13113
|
+
label: "Terminal lane",
|
|
13114
|
+
description: "A lane that owns one terminal. Opens at the first terminal node, closes when the run ends.",
|
|
13115
|
+
icon: "\u25A3",
|
|
13116
|
+
inputs: [],
|
|
13117
|
+
outputs: [],
|
|
13118
|
+
configSchema: [
|
|
13119
|
+
{ type: "text", key: "title", label: "Title", default: "Terminal" },
|
|
13120
|
+
{
|
|
13121
|
+
type: "text",
|
|
13122
|
+
key: "command",
|
|
13123
|
+
label: "Command",
|
|
13124
|
+
placeholder: "Leave empty for the default shell"
|
|
13125
|
+
},
|
|
13126
|
+
{ type: "text", key: "cwd", label: "Working directory", placeholder: "Host default" },
|
|
13127
|
+
{ type: "keyvalue", key: "env", label: "Environment" },
|
|
13128
|
+
{
|
|
13129
|
+
type: "select",
|
|
13130
|
+
key: "orientation",
|
|
13131
|
+
label: "Orientation",
|
|
13132
|
+
default: "horizontal",
|
|
13133
|
+
options: [
|
|
13134
|
+
{ value: "horizontal", label: "Row" },
|
|
13135
|
+
{ value: "vertical", label: "Column" }
|
|
13136
|
+
]
|
|
13137
|
+
}
|
|
13138
|
+
],
|
|
13139
|
+
defaultConfig: { title: "Terminal", orientation: "horizontal" },
|
|
13140
|
+
resizable: { minWidth: 220, minHeight: 120 },
|
|
13141
|
+
component: LaneNode
|
|
13142
|
+
},
|
|
13143
|
+
{
|
|
13144
|
+
// Terminal nodes. All three live inside a terminal lane and talk to the
|
|
13145
|
+
// session that lane owns; outside one they abort by name rather than
|
|
13146
|
+
// opening a shell of their own.
|
|
13147
|
+
name: "@particle-academy/terminal_run",
|
|
13148
|
+
aliases: ["terminal_run", "@fancy/terminal_run"],
|
|
13149
|
+
category: "io",
|
|
13150
|
+
label: "Run in terminal",
|
|
13151
|
+
description: "Run a shell command in the lane's terminal and wait for its exit code.",
|
|
13152
|
+
icon: "$",
|
|
13153
|
+
inputs: [{ id: "in" }],
|
|
13154
|
+
outputs: [{ id: "out", label: "output" }],
|
|
13155
|
+
executor: terminalRunExecutor,
|
|
13156
|
+
configSchema: [
|
|
13157
|
+
{
|
|
13158
|
+
type: "textarea",
|
|
13159
|
+
key: "command",
|
|
13160
|
+
label: "Command",
|
|
13161
|
+
rows: 3,
|
|
13162
|
+
required: true,
|
|
13163
|
+
placeholder: "npm test",
|
|
13164
|
+
description: "Runs in the lane's shell, so `cd` and exported variables persist between nodes. SHELL ONLY \u2014 a TUI never returns to a prompt, so use Send + Await for one."
|
|
13165
|
+
},
|
|
13166
|
+
{
|
|
13167
|
+
type: "number",
|
|
13168
|
+
key: "timeoutMs",
|
|
13169
|
+
label: "Timeout (ms)",
|
|
13170
|
+
default: 12e4,
|
|
13171
|
+
description: "How long to wait for the command to finish before failing the run."
|
|
13172
|
+
},
|
|
13173
|
+
{
|
|
13174
|
+
type: "switch",
|
|
13175
|
+
key: "failOnNonZero",
|
|
13176
|
+
label: "Fail the run on a non-zero exit",
|
|
13177
|
+
default: true,
|
|
13178
|
+
description: "On by default. Turning it off means a failed command lets the run report success \u2014 read the exit code on the output port and branch on it instead."
|
|
13179
|
+
}
|
|
13180
|
+
],
|
|
13181
|
+
defaultConfig: { command: "", timeoutMs: 12e4, failOnNonZero: true }
|
|
13182
|
+
},
|
|
13183
|
+
{
|
|
13184
|
+
name: "@particle-academy/terminal_send",
|
|
13185
|
+
aliases: ["terminal_send", "@fancy/terminal_send"],
|
|
13186
|
+
category: "io",
|
|
13187
|
+
label: "Send to terminal",
|
|
13188
|
+
description: "Type text at whatever is running in the lane's terminal, without waiting.",
|
|
13189
|
+
icon: "\u2328",
|
|
13190
|
+
inputs: [{ id: "in" }],
|
|
13191
|
+
outputs: [{ id: "out", label: "sent" }],
|
|
13192
|
+
executor: terminalSendExecutor,
|
|
13193
|
+
configSchema: [
|
|
13194
|
+
{
|
|
13195
|
+
type: "textarea",
|
|
13196
|
+
key: "text",
|
|
13197
|
+
label: "Text",
|
|
13198
|
+
rows: 4,
|
|
13199
|
+
placeholder: "Summarise the failing test and propose a fix.",
|
|
13200
|
+
description: "What to type. This is how a graph prompts an agent TUI such as Claude Code or Codex."
|
|
13201
|
+
},
|
|
13202
|
+
{
|
|
13203
|
+
type: "switch",
|
|
13204
|
+
key: "submit",
|
|
13205
|
+
label: "Press Enter",
|
|
13206
|
+
default: true,
|
|
13207
|
+
description: "Off leaves the text on the input line \u2014 useful for building one up across several nodes."
|
|
13208
|
+
},
|
|
13209
|
+
{
|
|
13210
|
+
type: "switch",
|
|
13211
|
+
key: "clearFirst",
|
|
13212
|
+
label: "Forget earlier output first",
|
|
13213
|
+
default: false,
|
|
13214
|
+
description: "Discards anything the terminal said before this send, so a following Await cannot match a prompt left over from the previous exchange."
|
|
13215
|
+
}
|
|
13216
|
+
],
|
|
13217
|
+
defaultConfig: { text: "", submit: true, clearFirst: false }
|
|
13218
|
+
},
|
|
13219
|
+
{
|
|
13220
|
+
name: "@particle-academy/terminal_await",
|
|
13221
|
+
aliases: ["terminal_await", "@fancy/terminal_await"],
|
|
13222
|
+
category: "io",
|
|
13223
|
+
label: "Await terminal output",
|
|
13224
|
+
description: "Wait until the lane's terminal prints something that matches.",
|
|
13225
|
+
icon: "\u23F1",
|
|
13226
|
+
inputs: [{ id: "in" }],
|
|
13227
|
+
outputs: [{ id: "out", label: "output" }],
|
|
13228
|
+
executor: terminalAwaitExecutor,
|
|
13229
|
+
configSchema: [
|
|
13230
|
+
{
|
|
13231
|
+
type: "text",
|
|
13232
|
+
key: "pattern",
|
|
13233
|
+
label: "Wait for",
|
|
13234
|
+
required: true,
|
|
13235
|
+
placeholder: "esc to interrupt",
|
|
13236
|
+
description: "Matched against the output with colour codes already stripped, so match what you SEE."
|
|
13237
|
+
},
|
|
13238
|
+
{
|
|
13239
|
+
type: "select",
|
|
13240
|
+
key: "mode",
|
|
13241
|
+
label: "Match as",
|
|
13242
|
+
default: "text",
|
|
13243
|
+
options: [
|
|
13244
|
+
{ value: "text", label: "Plain text" },
|
|
13245
|
+
{ value: "regex", label: "Regular expression" }
|
|
13246
|
+
],
|
|
13247
|
+
description: "Regex mode also returns capture groups, which is how a value gets out of a prompt."
|
|
13248
|
+
},
|
|
13249
|
+
{
|
|
13250
|
+
type: "number",
|
|
13251
|
+
key: "timeoutMs",
|
|
13252
|
+
label: "Timeout (ms)",
|
|
13253
|
+
default: 12e4
|
|
13254
|
+
},
|
|
13255
|
+
{
|
|
13256
|
+
type: "select",
|
|
13257
|
+
key: "onTimeout",
|
|
13258
|
+
label: "If it never appears",
|
|
13259
|
+
default: "fail",
|
|
13260
|
+
options: [
|
|
13261
|
+
{ value: "fail", label: "Fail the run" },
|
|
13262
|
+
{ value: "continue", label: "Continue with matched: false" }
|
|
13263
|
+
],
|
|
13264
|
+
description: "Failing is the default. Continuing lets the next node type at a process that never became ready while the run still reports success, so it has to be asked for."
|
|
13265
|
+
}
|
|
13266
|
+
],
|
|
13267
|
+
defaultConfig: { pattern: "", mode: "text", timeoutMs: 12e4, onTimeout: "fail" }
|
|
13268
|
+
},
|
|
12458
13269
|
{
|
|
12459
13270
|
// Note — a sticky-note annotation. Portless + visual-only: the runtime skips
|
|
12460
13271
|
// the `annotation` category, so a note's text NEVER reaches a runner — it
|
|
@@ -13214,7 +14025,7 @@ var CATEGORY_LABELS = {
|
|
|
13214
14025
|
annotation: "Notes",
|
|
13215
14026
|
custom: "Custom"
|
|
13216
14027
|
};
|
|
13217
|
-
function NodePalette({ categories, onPick, className, style: style2 }) {
|
|
14028
|
+
function NodePalette({ categories, kindFilter, onPick, className, style: style2 }) {
|
|
13218
14029
|
const [, setRev] = ReactExports.useState(0);
|
|
13219
14030
|
ReactExports.useEffect(() => onNodeKindsChanged(() => setRev((n) => n + 1)), []);
|
|
13220
14031
|
const [query, setQuery] = ReactExports.useState("");
|
|
@@ -13222,7 +14033,8 @@ function NodePalette({ categories, onPick, className, style: style2 }) {
|
|
|
13222
14033
|
const grouped = ReactExports.useMemo(() => {
|
|
13223
14034
|
const all = listNodeKinds();
|
|
13224
14035
|
const q2 = query.trim().toLowerCase();
|
|
13225
|
-
const
|
|
14036
|
+
const allowed = kindFilter ? all.filter((k2) => kindFilter({ kind: k2 })) : all;
|
|
14037
|
+
const filtered = q2 ? allowed.filter((k2) => k2.name.includes(q2) || k2.label.toLowerCase().includes(q2) || (k2.description ?? "").toLowerCase().includes(q2)) : allowed;
|
|
13226
14038
|
const map = /* @__PURE__ */ new Map();
|
|
13227
14039
|
for (const cat of visibleCats) map.set(cat, []);
|
|
13228
14040
|
for (const k2 of filtered) {
|
|
@@ -13230,7 +14042,7 @@ function NodePalette({ categories, onPick, className, style: style2 }) {
|
|
|
13230
14042
|
map.get(k2.category).push(k2);
|
|
13231
14043
|
}
|
|
13232
14044
|
return map;
|
|
13233
|
-
}, [query, visibleCats]);
|
|
14045
|
+
}, [query, visibleCats, kindFilter]);
|
|
13234
14046
|
return /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: ["ff-palette", className ?? ""].filter(Boolean).join(" "), style: style2, children: [
|
|
13235
14047
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-palette__search", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
13236
14048
|
"input",
|
|
@@ -13355,8 +14167,8 @@ function describeExpressionGrammar() {
|
|
|
13355
14167
|
note: "Dot-paths only \u2014 no arithmetic, comparisons or function calls; anything else resolves to nothing. A field that is exactly one expression keeps the resolved value's type; mixed text interpolates as a string. fancy-flow's own runFlow does not interpolate: it hands config to your executor verbatim, and the fancy-flow-php runtime is what resolves these."
|
|
13356
14168
|
};
|
|
13357
14169
|
}
|
|
13358
|
-
function findTrigger(
|
|
13359
|
-
const before =
|
|
14170
|
+
function findTrigger(text3, caret) {
|
|
14171
|
+
const before = text3.slice(0, caret);
|
|
13360
14172
|
const open = before.lastIndexOf("{{");
|
|
13361
14173
|
if (open === -1) return null;
|
|
13362
14174
|
if (before.indexOf("}}", open) !== -1) return null;
|
|
@@ -13367,9 +14179,9 @@ function filterVariables(vars, query) {
|
|
|
13367
14179
|
if (q2 === "") return vars;
|
|
13368
14180
|
return vars.filter((v2) => v2.path.toLowerCase().includes(q2));
|
|
13369
14181
|
}
|
|
13370
|
-
function applyCompletion(
|
|
13371
|
-
const head =
|
|
13372
|
-
const tail =
|
|
14182
|
+
function applyCompletion(text3, caret, trigger, variable) {
|
|
14183
|
+
const head = text3.slice(0, trigger.open);
|
|
14184
|
+
const tail = text3.slice(caret);
|
|
13373
14185
|
return { value: `${head}${variable.expression}${tail}`, caret: head.length + variable.expression.length };
|
|
13374
14186
|
}
|
|
13375
14187
|
function ExpressionField({
|
|
@@ -13893,7 +14705,7 @@ function JsonField({
|
|
|
13893
14705
|
return "";
|
|
13894
14706
|
}
|
|
13895
14707
|
}, [value]);
|
|
13896
|
-
const [
|
|
14708
|
+
const [text3, setText] = ReactExports.useState(serialized);
|
|
13897
14709
|
const [error, setError] = ReactExports.useState(null);
|
|
13898
14710
|
const [touched, setTouched] = ReactExports.useState(false);
|
|
13899
14711
|
const lastProps = ReactExports.useRef(serialized);
|
|
@@ -13931,7 +14743,7 @@ function JsonField({
|
|
|
13931
14743
|
...handle,
|
|
13932
14744
|
className: "ff-panel__input ff-panel__input--json",
|
|
13933
14745
|
rows: rows ?? 6,
|
|
13934
|
-
value:
|
|
14746
|
+
value: text3,
|
|
13935
14747
|
spellCheck: false,
|
|
13936
14748
|
"aria-invalid": showError || void 0,
|
|
13937
14749
|
"aria-describedby": showError ? errorId : void 0,
|
|
@@ -14996,6 +15808,7 @@ function FlowEditorInner({
|
|
|
14996
15808
|
showPanel = true,
|
|
14997
15809
|
fieldRenderers,
|
|
14998
15810
|
fieldFilter,
|
|
15811
|
+
kindFilter,
|
|
14999
15812
|
showFeed = true,
|
|
15000
15813
|
extraToolbar,
|
|
15001
15814
|
actions = [],
|
|
@@ -15016,7 +15829,7 @@ function FlowEditorInner({
|
|
|
15016
15829
|
const [prompt, setPrompt] = ReactExports.useState(null);
|
|
15017
15830
|
const humanExecutors = ReactExports.useMemo(
|
|
15018
15831
|
() => ({
|
|
15019
|
-
user_input: ({ node }) => new Promise((
|
|
15832
|
+
user_input: ({ node }) => new Promise((resolve2) => {
|
|
15020
15833
|
const cfg = node.data?.config ?? {};
|
|
15021
15834
|
setPrompt({
|
|
15022
15835
|
kind: "input",
|
|
@@ -15025,11 +15838,11 @@ function FlowEditorInner({
|
|
|
15025
15838
|
fields: humanInputFields(cfg),
|
|
15026
15839
|
resolve: (values) => {
|
|
15027
15840
|
setPrompt(null);
|
|
15028
|
-
|
|
15841
|
+
resolve2(values);
|
|
15029
15842
|
}
|
|
15030
15843
|
});
|
|
15031
15844
|
}),
|
|
15032
|
-
human_approval: ({ node }) => new Promise((
|
|
15845
|
+
human_approval: ({ node }) => new Promise((resolve2) => {
|
|
15033
15846
|
const cfg = node.data?.config ?? {};
|
|
15034
15847
|
setPrompt({
|
|
15035
15848
|
kind: "approval",
|
|
@@ -15037,7 +15850,7 @@ function FlowEditorInner({
|
|
|
15037
15850
|
description: typeof cfg.description === "string" ? cfg.description : void 0,
|
|
15038
15851
|
resolve: (approved) => {
|
|
15039
15852
|
setPrompt(null);
|
|
15040
|
-
|
|
15853
|
+
resolve2({ branch: approved ? "approved" : "denied" });
|
|
15041
15854
|
}
|
|
15042
15855
|
});
|
|
15043
15856
|
})
|
|
@@ -15423,7 +16236,7 @@ function FlowEditorInner({
|
|
|
15423
16236
|
] })
|
|
15424
16237
|
] });
|
|
15425
16238
|
return /* @__PURE__ */ jsxRuntime.jsxs(FlowEditorProvider, { value: api, children: [
|
|
15426
|
-
showPalette && (slots.palette ? slots.palette(api) : /* @__PURE__ */ jsxRuntime.jsx(NodePalette, { className: "ff-editor__palette" })),
|
|
16239
|
+
showPalette && (slots.palette ? slots.palette(api) : /* @__PURE__ */ jsxRuntime.jsx(NodePalette, { className: "ff-editor__palette", kindFilter })),
|
|
15427
16240
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-editor__main", ...dropHandlers, children: [
|
|
15428
16241
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
15429
16242
|
FlowCanvas,
|
|
@@ -15536,8 +16349,8 @@ function FlowEditorInner({
|
|
|
15536
16349
|
x: labelEdit.x,
|
|
15537
16350
|
y: labelEdit.y,
|
|
15538
16351
|
initial: flow.edges.find((e) => e.id === labelEdit.edgeId)?.label ?? "",
|
|
15539
|
-
onCommit: (
|
|
15540
|
-
api.setEdgeLabel(labelEdit.edgeId,
|
|
16352
|
+
onCommit: (text3) => {
|
|
16353
|
+
api.setEdgeLabel(labelEdit.edgeId, text3);
|
|
15541
16354
|
setLabelEdit(null);
|
|
15542
16355
|
},
|
|
15543
16356
|
onCancel: () => setLabelEdit(null)
|
|
@@ -15650,7 +16463,7 @@ function EdgeLabelEditor({
|
|
|
15650
16463
|
onCommit,
|
|
15651
16464
|
onCancel
|
|
15652
16465
|
}) {
|
|
15653
|
-
const [
|
|
16466
|
+
const [text3, setText] = ReactExports.useState(initial);
|
|
15654
16467
|
const ref = ReactExports.useRef(null);
|
|
15655
16468
|
ReactExports.useEffect(() => {
|
|
15656
16469
|
ref.current?.focus();
|
|
@@ -15667,17 +16480,17 @@ function EdgeLabelEditor({
|
|
|
15667
16480
|
{
|
|
15668
16481
|
ref,
|
|
15669
16482
|
className: "ff-panel__input",
|
|
15670
|
-
value:
|
|
16483
|
+
value: text3,
|
|
15671
16484
|
placeholder: "Label this connection",
|
|
15672
16485
|
"aria-label": "Connection label",
|
|
15673
16486
|
"data-action": "edge-label-input",
|
|
15674
16487
|
onChange: (e) => setText(e.target.value),
|
|
15675
16488
|
onKeyDown: (e) => {
|
|
15676
16489
|
e.stopPropagation();
|
|
15677
|
-
if (e.key === "Enter") onCommit(
|
|
16490
|
+
if (e.key === "Enter") onCommit(text3);
|
|
15678
16491
|
if (e.key === "Escape") onCancel();
|
|
15679
16492
|
},
|
|
15680
|
-
onBlur: () => onCommit(
|
|
16493
|
+
onBlur: () => onCommit(text3)
|
|
15681
16494
|
}
|
|
15682
16495
|
)
|
|
15683
16496
|
}
|
|
@@ -15914,106 +16727,6 @@ var flowKeys = {
|
|
|
15914
16727
|
run: (runId) => ["flow", "runs", runId]
|
|
15915
16728
|
};
|
|
15916
16729
|
|
|
15917
|
-
// src/expressions/expr.ts
|
|
15918
|
-
function wholeExpression(trimmed) {
|
|
15919
|
-
if (trimmed.length < 4) return null;
|
|
15920
|
-
if (!trimmed.startsWith("{{") || !trimmed.endsWith("}}")) return null;
|
|
15921
|
-
return trimmed.slice(2, -2);
|
|
15922
|
-
}
|
|
15923
|
-
function interpolate(template, resolve) {
|
|
15924
|
-
let out = "";
|
|
15925
|
-
let i = 0;
|
|
15926
|
-
for (; ; ) {
|
|
15927
|
-
const open = template.indexOf("{{", i);
|
|
15928
|
-
if (open === -1) return out + template.slice(i);
|
|
15929
|
-
const close = template.indexOf("}}", open + 2);
|
|
15930
|
-
if (close === -1) return out + template.slice(i);
|
|
15931
|
-
out += template.slice(i, open) + resolve(template.slice(open + 2, close));
|
|
15932
|
-
i = close + 2;
|
|
15933
|
-
}
|
|
15934
|
-
}
|
|
15935
|
-
var FALSY_STRINGS = /* @__PURE__ */ new Set(["", "0", "false", "no", "off", "null"]);
|
|
15936
|
-
var UnresolvedPathError = class extends Error {
|
|
15937
|
-
constructor(path) {
|
|
15938
|
-
super(
|
|
15939
|
-
`Expression path "${path}" did not resolve. Under the "throw" policy an unresolvable path is an error rather than an empty string.`
|
|
15940
|
-
);
|
|
15941
|
-
this.path = path;
|
|
15942
|
-
this.name = "UnresolvedPathError";
|
|
15943
|
-
}
|
|
15944
|
-
};
|
|
15945
|
-
function tryResolvePath(path, context) {
|
|
15946
|
-
const unresolved = { resolved: false, value: null };
|
|
15947
|
-
const trimmed = path.trim();
|
|
15948
|
-
if (trimmed === "") return unresolved;
|
|
15949
|
-
const segments = trimmed.split(".");
|
|
15950
|
-
let cursor;
|
|
15951
|
-
const head = segments[0];
|
|
15952
|
-
if (head === "$json" || head === "$input") {
|
|
15953
|
-
cursor = context !== null && typeof context === "object" && "in" in context ? context.in : context;
|
|
15954
|
-
segments.shift();
|
|
15955
|
-
} else {
|
|
15956
|
-
cursor = context;
|
|
15957
|
-
}
|
|
15958
|
-
for (const segment of segments) {
|
|
15959
|
-
if (cursor === null || cursor === void 0) return unresolved;
|
|
15960
|
-
if (typeof cursor !== "object") return unresolved;
|
|
15961
|
-
const next = cursor[segment];
|
|
15962
|
-
if (next === void 0) return unresolved;
|
|
15963
|
-
cursor = next;
|
|
15964
|
-
}
|
|
15965
|
-
return { resolved: true, value: cursor === void 0 ? null : cursor };
|
|
15966
|
-
}
|
|
15967
|
-
function resolvePath(path, context) {
|
|
15968
|
-
return tryResolvePath(path, context).value;
|
|
15969
|
-
}
|
|
15970
|
-
function evaluateExpression(template, context, options = {}) {
|
|
15971
|
-
if (typeof template !== "string") return template;
|
|
15972
|
-
const policy = options.onUnresolved ?? "empty";
|
|
15973
|
-
const whole = wholeExpression(template.trim());
|
|
15974
|
-
if (whole !== null) {
|
|
15975
|
-
const r = tryResolvePath(whole, context);
|
|
15976
|
-
if (r.resolved) return r.value;
|
|
15977
|
-
if (policy === "throw") throw new UnresolvedPathError(whole);
|
|
15978
|
-
return policy === "keep" ? template : null;
|
|
15979
|
-
}
|
|
15980
|
-
return interpolate(template, (path) => {
|
|
15981
|
-
const r = tryResolvePath(path, context);
|
|
15982
|
-
if (r.resolved) return stringify(r.value);
|
|
15983
|
-
if (policy === "throw") throw new UnresolvedPathError(path);
|
|
15984
|
-
return policy === "keep" ? `{{${path}}}` : "";
|
|
15985
|
-
});
|
|
15986
|
-
}
|
|
15987
|
-
function truthy(value) {
|
|
15988
|
-
if (typeof value === "boolean") return value;
|
|
15989
|
-
if (value === null || value === void 0) return false;
|
|
15990
|
-
if (typeof value === "string") return !FALSY_STRINGS.has(value.trim().toLowerCase());
|
|
15991
|
-
if (Array.isArray(value)) return value.length > 0;
|
|
15992
|
-
if (typeof value === "number") return value !== 0;
|
|
15993
|
-
return Boolean(value);
|
|
15994
|
-
}
|
|
15995
|
-
function text(value) {
|
|
15996
|
-
return stringify(value);
|
|
15997
|
-
}
|
|
15998
|
-
function stringify(value) {
|
|
15999
|
-
if (typeof value === "string") return value;
|
|
16000
|
-
if (typeof value === "boolean") return value ? "true" : "false";
|
|
16001
|
-
if (value === null || value === void 0) return "";
|
|
16002
|
-
if (typeof value === "number" || typeof value === "bigint") return String(value);
|
|
16003
|
-
try {
|
|
16004
|
-
return JSON.stringify(value) ?? "";
|
|
16005
|
-
} catch {
|
|
16006
|
-
return "";
|
|
16007
|
-
}
|
|
16008
|
-
}
|
|
16009
|
-
function evaluateConfig(config, context, options = {}) {
|
|
16010
|
-
const out = {};
|
|
16011
|
-
for (const [key, value] of Object.entries(config)) {
|
|
16012
|
-
out[key] = Array.isArray(value) ? value.map((v2) => evaluateExpression(v2, context, options)) : value !== null && typeof value === "object" ? evaluateConfig(value, context, options) : evaluateExpression(value, context, options);
|
|
16013
|
-
}
|
|
16014
|
-
return out;
|
|
16015
|
-
}
|
|
16016
|
-
|
|
16017
16730
|
// src/index.ts
|
|
16018
16731
|
ensureBuiltinKinds();
|
|
16019
16732
|
/*! Bundled license information:
|