@particle-academy/fancy-flow 0.65.1 → 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.
Files changed (106) hide show
  1. package/README.md +100 -3
  2. package/dist/{FlowViewer-BI_AyyvU.d.ts → FlowViewer-BCc_BvuD.d.ts} +1 -1
  3. package/dist/{FlowViewer-DABx-6A-.d.cts → FlowViewer-CoAJ3o52.d.cts} +1 -1
  4. package/dist/{HumanPrompt-Bpp3JiVH.d.cts → HumanPrompt--pXDOyYC.d.cts} +2 -2
  5. package/dist/{HumanPrompt-BKThiyjB.d.ts → HumanPrompt-DXm_FP9z.d.ts} +2 -2
  6. package/dist/chunk-2QXKDLGT.js +58 -0
  7. package/dist/chunk-2QXKDLGT.js.map +1 -0
  8. package/dist/{chunk-77V4QC6Y.js → chunk-B7HKJJVV.js} +3 -3
  9. package/dist/{chunk-77V4QC6Y.js.map → chunk-B7HKJJVV.js.map} +1 -1
  10. package/dist/{chunk-MBVX4ZRB.js → chunk-FNYLKSFJ.js} +3 -3
  11. package/dist/{chunk-MBVX4ZRB.js.map → chunk-FNYLKSFJ.js.map} +1 -1
  12. package/dist/{chunk-JF6WCRBU.js → chunk-HQGOFGAL.js} +861 -40
  13. package/dist/chunk-HQGOFGAL.js.map +1 -0
  14. package/dist/{chunk-5H54OTKT.js → chunk-IEYCNFXZ.js} +3 -3
  15. package/dist/{chunk-5H54OTKT.js.map → chunk-IEYCNFXZ.js.map} +1 -1
  16. package/dist/{chunk-W5DPKJY4.js → chunk-MTONBM53.js} +4 -4
  17. package/dist/chunk-MTONBM53.js.map +1 -0
  18. package/dist/{chunk-RIAFHQT5.js → chunk-PNHZHEWE.js} +3 -3
  19. package/dist/{chunk-RIAFHQT5.js.map → chunk-PNHZHEWE.js.map} +1 -1
  20. package/dist/{chunk-EO6444T2.js → chunk-UCCTXJXL.js} +4 -4
  21. package/dist/{chunk-EO6444T2.js.map → chunk-UCCTXJXL.js.map} +1 -1
  22. package/dist/connectors.d.cts +2 -2
  23. package/dist/connectors.d.ts +2 -2
  24. package/dist/durable/index.d.cts +2 -2
  25. package/dist/durable/index.d.ts +2 -2
  26. package/dist/durable.cjs +837 -47
  27. package/dist/durable.cjs.map +1 -1
  28. package/dist/durable.js +2 -3
  29. package/dist/durable.js.map +1 -1
  30. package/dist/engine.cjs +843 -132
  31. package/dist/engine.cjs.map +1 -1
  32. package/dist/engine.d.cts +6 -7
  33. package/dist/engine.d.ts +6 -7
  34. package/dist/engine.js +5 -6
  35. package/dist/engine.js.map +1 -1
  36. package/dist/fields/react-fancy.d.cts +3 -3
  37. package/dist/fields/react-fancy.d.ts +3 -3
  38. package/dist/index.cjs +853 -140
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.cts +65 -36
  41. package/dist/index.d.ts +65 -36
  42. package/dist/index.js +19 -18
  43. package/dist/index.js.map +1 -1
  44. package/dist/layout/index.d.cts +1 -1
  45. package/dist/layout/index.d.ts +1 -1
  46. package/dist/llm/prism.cjs.map +1 -1
  47. package/dist/llm/prism.d.cts +1 -2
  48. package/dist/llm/prism.d.ts +1 -2
  49. package/dist/llm/prism.js +1 -1
  50. package/dist/llm/vercel-ai.cjs.map +1 -1
  51. package/dist/llm/vercel-ai.d.cts +1 -2
  52. package/dist/llm/vercel-ai.d.ts +1 -2
  53. package/dist/llm/vercel-ai.js +1 -1
  54. package/dist/registry/index.d.cts +5 -6
  55. package/dist/registry/index.d.ts +5 -6
  56. package/dist/{registry-CntAdUcY.d.cts → registry-BcgllSk2.d.cts} +1 -1
  57. package/dist/{registry-DYoV-MQi.d.ts → registry-h-2QswZ5.d.ts} +1 -1
  58. package/dist/registry.cjs +867 -37
  59. package/dist/registry.cjs.map +1 -1
  60. package/dist/registry.js +3 -3
  61. package/dist/{run-cohort-C1nRh_mi.d.ts → run-cohort-CjUBWg6X.d.ts} +2 -2
  62. package/dist/{run-cohort-CBRAUj-z.d.cts → run-cohort-DL9JzzPU.d.cts} +2 -2
  63. package/dist/{run-flow-2XsHBwpd.d.cts → run-flow-B_8hgO5_.d.cts} +1 -1
  64. package/dist/{run-flow-D7AOCcfE.d.ts → run-flow-CxEGBOxd.d.ts} +1 -1
  65. package/dist/runtime/index.d.cts +5 -5
  66. package/dist/runtime/index.d.ts +5 -5
  67. package/dist/runtime.cjs +837 -36
  68. package/dist/runtime.cjs.map +1 -1
  69. package/dist/runtime.js +4 -4
  70. package/dist/schema/index.d.cts +19 -6
  71. package/dist/schema/index.d.ts +19 -6
  72. package/dist/schema.cjs +837 -36
  73. package/dist/schema.cjs.map +1 -1
  74. package/dist/schema.js +4 -4
  75. package/dist/screens.cjs +837 -36
  76. package/dist/screens.cjs.map +1 -1
  77. package/dist/screens.d.cts +2 -2
  78. package/dist/screens.d.ts +2 -2
  79. package/dist/screens.js +5 -5
  80. package/dist/terminal/fancy-term-host.cjs +99 -0
  81. package/dist/terminal/fancy-term-host.cjs.map +1 -0
  82. package/dist/terminal/fancy-term-host.d.cts +76 -0
  83. package/dist/terminal/fancy-term-host.d.ts +76 -0
  84. package/dist/terminal/fancy-term-host.js +92 -0
  85. package/dist/terminal/fancy-term-host.js.map +1 -0
  86. package/dist/types-B-Syk9-M.d.cts +620 -0
  87. package/dist/types-B-Syk9-M.d.ts +620 -0
  88. package/dist/{types-D71SKA5A.d.cts → types-C2ATTmjN.d.cts} +1 -1
  89. package/dist/{types-Ckhz-YwC.d.ts → types-VunLGqrn.d.ts} +1 -1
  90. package/dist/ux.cjs +845 -39
  91. package/dist/ux.cjs.map +1 -1
  92. package/dist/ux.d.cts +14 -2
  93. package/dist/ux.d.ts +14 -2
  94. package/dist/ux.js +10 -5
  95. package/dist/ux.js.map +1 -1
  96. package/package.json +11 -1
  97. package/dist/capabilities-BYa5p5jw.d.cts +0 -112
  98. package/dist/capabilities-COOXRiNL.d.ts +0 -112
  99. package/dist/chunk-JF6WCRBU.js.map +0 -1
  100. package/dist/chunk-UM4C46AF.js +0 -103
  101. package/dist/chunk-UM4C46AF.js.map +0 -1
  102. package/dist/chunk-USL4FMFU.js +0 -41
  103. package/dist/chunk-USL4FMFU.js.map +0 -1
  104. package/dist/chunk-W5DPKJY4.js.map +0 -1
  105. package/dist/types-JFYjPJAG.d.cts +0 -333
  106. package/dist/types-JFYjPJAG.d.ts +0 -333
package/dist/durable.cjs CHANGED
@@ -2269,38 +2269,38 @@ function attrRemoveNS2(fullname) {
2269
2269
  this.removeAttributeNS(fullname.space, fullname.local);
2270
2270
  };
2271
2271
  }
2272
- function attrConstant2(name, interpolate, value1) {
2272
+ function attrConstant2(name, interpolate2, value1) {
2273
2273
  var string00, string1 = value1 + "", interpolate0;
2274
2274
  return function() {
2275
2275
  var string0 = this.getAttribute(name);
2276
- return string0 === string1 ? null : string0 === string00 ? interpolate0 : interpolate0 = interpolate(string00 = string0, value1);
2276
+ return string0 === string1 ? null : string0 === string00 ? interpolate0 : interpolate0 = interpolate2(string00 = string0, value1);
2277
2277
  };
2278
2278
  }
2279
- function attrConstantNS2(fullname, interpolate, value1) {
2279
+ function attrConstantNS2(fullname, interpolate2, value1) {
2280
2280
  var string00, string1 = value1 + "", interpolate0;
2281
2281
  return function() {
2282
2282
  var string0 = this.getAttributeNS(fullname.space, fullname.local);
2283
- return string0 === string1 ? null : string0 === string00 ? interpolate0 : interpolate0 = interpolate(string00 = string0, value1);
2283
+ return string0 === string1 ? null : string0 === string00 ? interpolate0 : interpolate0 = interpolate2(string00 = string0, value1);
2284
2284
  };
2285
2285
  }
2286
- function attrFunction2(name, interpolate, value) {
2286
+ function attrFunction2(name, interpolate2, value) {
2287
2287
  var string00, string10, interpolate0;
2288
2288
  return function() {
2289
2289
  var string0, value1 = value(this), string1;
2290
2290
  if (value1 == null) return void this.removeAttribute(name);
2291
2291
  string0 = this.getAttribute(name);
2292
2292
  string1 = value1 + "";
2293
- return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));
2293
+ return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : (string10 = string1, interpolate0 = interpolate2(string00 = string0, value1));
2294
2294
  };
2295
2295
  }
2296
- function attrFunctionNS2(fullname, interpolate, value) {
2296
+ function attrFunctionNS2(fullname, interpolate2, value) {
2297
2297
  var string00, string10, interpolate0;
2298
2298
  return function() {
2299
2299
  var string0, value1 = value(this), string1;
2300
2300
  if (value1 == null) return void this.removeAttributeNS(fullname.space, fullname.local);
2301
2301
  string0 = this.getAttributeNS(fullname.space, fullname.local);
2302
2302
  string1 = value1 + "";
2303
- return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));
2303
+ return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : (string10 = string1, interpolate0 = interpolate2(string00 = string0, value1));
2304
2304
  };
2305
2305
  }
2306
2306
  function attr_default2(name, value) {
@@ -2510,11 +2510,11 @@ function selection_default2() {
2510
2510
  }
2511
2511
 
2512
2512
  // node_modules/d3-transition/src/transition/style.js
2513
- function styleNull(name, interpolate) {
2513
+ function styleNull(name, interpolate2) {
2514
2514
  var string00, string10, interpolate0;
2515
2515
  return function() {
2516
2516
  var string0 = styleValue(this, name), string1 = (this.style.removeProperty(name), styleValue(this, name));
2517
- return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : interpolate0 = interpolate(string00 = string0, string10 = string1);
2517
+ return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : interpolate0 = interpolate2(string00 = string0, string10 = string1);
2518
2518
  };
2519
2519
  }
2520
2520
  function styleRemove2(name) {
@@ -2522,19 +2522,19 @@ function styleRemove2(name) {
2522
2522
  this.style.removeProperty(name);
2523
2523
  };
2524
2524
  }
2525
- function styleConstant2(name, interpolate, value1) {
2525
+ function styleConstant2(name, interpolate2, value1) {
2526
2526
  var string00, string1 = value1 + "", interpolate0;
2527
2527
  return function() {
2528
2528
  var string0 = styleValue(this, name);
2529
- return string0 === string1 ? null : string0 === string00 ? interpolate0 : interpolate0 = interpolate(string00 = string0, value1);
2529
+ return string0 === string1 ? null : string0 === string00 ? interpolate0 : interpolate0 = interpolate2(string00 = string0, value1);
2530
2530
  };
2531
2531
  }
2532
- function styleFunction2(name, interpolate, value) {
2532
+ function styleFunction2(name, interpolate2, value) {
2533
2533
  var string00, string10, interpolate0;
2534
2534
  return function() {
2535
2535
  var string0 = styleValue(this, name), value1 = value(this), string1 = value1 + "";
2536
2536
  if (value1 == null) string1 = value1 = (this.style.removeProperty(name), styleValue(this, name));
2537
- return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));
2537
+ return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : (string10 = string1, interpolate0 = interpolate2(string00 = string0, value1));
2538
2538
  };
2539
2539
  }
2540
2540
  function styleMaybeRemove(id2, name) {
@@ -2636,9 +2636,9 @@ function transition_default() {
2636
2636
  // node_modules/d3-transition/src/transition/end.js
2637
2637
  function end_default() {
2638
2638
  var on0, on1, that = this, id2 = that._id, size = that.size();
2639
- return new Promise(function(resolve, reject) {
2639
+ return new Promise(function(resolve2, reject) {
2640
2640
  var cancel = { value: reject }, end = { value: function() {
2641
- if (--size === 0) resolve();
2641
+ if (--size === 0) resolve2();
2642
2642
  } };
2643
2643
  that.each(function() {
2644
2644
  var schedule = set2(this, id2), on = schedule.on;
@@ -2650,7 +2650,7 @@ function end_default() {
2650
2650
  }
2651
2651
  schedule.on = on1;
2652
2652
  });
2653
- if (size === 0) resolve();
2653
+ if (size === 0) resolve2();
2654
2654
  });
2655
2655
  }
2656
2656
 
@@ -2852,7 +2852,7 @@ function defaultConstrain(transform2, extent, translateExtent) {
2852
2852
  );
2853
2853
  }
2854
2854
  function zoom_default2() {
2855
- var filter2 = defaultFilter2, extent = defaultExtent, constrain = defaultConstrain, wheelDelta2 = defaultWheelDelta, touchable = defaultTouchable2, scaleExtent = [0, Infinity], translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]], duration = 250, interpolate = zoom_default, listeners2 = dispatch_default("start", "zoom", "end"), touchstarting, touchfirst, touchending, touchDelay = 500, wheelDelay = 150, clickDistance2 = 0, tapDistance = 10;
2855
+ var filter2 = defaultFilter2, extent = defaultExtent, constrain = defaultConstrain, wheelDelta2 = defaultWheelDelta, touchable = defaultTouchable2, scaleExtent = [0, Infinity], translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]], duration = 250, interpolate2 = zoom_default, listeners2 = dispatch_default("start", "zoom", "end"), touchstarting, touchfirst, touchending, touchDelay = 500, wheelDelay = 150, clickDistance2 = 0, tapDistance = 10;
2856
2856
  function zoom(selection2) {
2857
2857
  selection2.property("__zoom", defaultTransform).on("wheel.zoom", wheeled, { passive: false }).on("mousedown.zoom", mousedowned).on("dblclick.zoom", dblclicked).filter(touchable).on("touchstart.zoom", touchstarted).on("touchmove.zoom", touchmoved).on("touchend.zoom touchcancel.zoom", touchended).style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
2858
2858
  }
@@ -2913,7 +2913,7 @@ function zoom_default2() {
2913
2913
  }).on("interrupt.zoom end.zoom", function() {
2914
2914
  gesture(this, arguments).event(event).end();
2915
2915
  }).tween("zoom", function() {
2916
- var that = this, args = arguments, g = gesture(that, args).event(event), e = extent.apply(that, args), p = point == null ? centroid(e) : typeof point === "function" ? point.apply(that, args) : point, w = Math.max(e[1][0] - e[0][0], e[1][1] - e[0][1]), a = that.__zoom, b = typeof transform2 === "function" ? transform2.apply(that, args) : transform2, i = interpolate(a.invert(p).concat(w / a.k), b.invert(p).concat(w / b.k));
2916
+ var that = this, args = arguments, g = gesture(that, args).event(event), e = extent.apply(that, args), p = point == null ? centroid(e) : typeof point === "function" ? point.apply(that, args) : point, w = Math.max(e[1][0] - e[0][0], e[1][1] - e[0][1]), a = that.__zoom, b = typeof transform2 === "function" ? transform2.apply(that, args) : transform2, i = interpolate2(a.invert(p).concat(w / a.k), b.invert(p).concat(w / b.k));
2917
2917
  return function(t) {
2918
2918
  if (t === 1) t = b;
2919
2919
  else {
@@ -3118,7 +3118,7 @@ function zoom_default2() {
3118
3118
  return arguments.length ? (duration = +_, zoom) : duration;
3119
3119
  };
3120
3120
  zoom.interpolate = function(_) {
3121
- return arguments.length ? (interpolate = _, zoom) : interpolate;
3121
+ return arguments.length ? (interpolate2 = _, zoom) : interpolate2;
3122
3122
  };
3123
3123
  zoom.on = function() {
3124
3124
  var value = listeners2.on.apply(listeners2, arguments);
@@ -3611,13 +3611,13 @@ function areSetsEqual(a, b) {
3611
3611
  return true;
3612
3612
  }
3613
3613
  function withResolvers() {
3614
- let resolve;
3614
+ let resolve2;
3615
3615
  let reject;
3616
3616
  const promise = new Promise((res, rej) => {
3617
- resolve = res;
3617
+ resolve2 = res;
3618
3618
  reject = rej;
3619
3619
  });
3620
- return { promise, resolve, reject };
3620
+ return { promise, resolve: resolve2, reject };
3621
3621
  }
3622
3622
  function mergeAriaLabelConfig(partial) {
3623
3623
  return { ...defaultAriaLabelConfig, ...partial || {} };
@@ -5233,8 +5233,8 @@ function XYPanZoom({ domNode, minZoom, maxZoom, translateExtent, viewport, onPan
5233
5233
  d3ZoomInstance.wheelDelta(wheelDelta);
5234
5234
  async function setTransform(transform2, options) {
5235
5235
  if (d3Selection) {
5236
- return new Promise((resolve) => {
5237
- d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).transform(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)), transform2);
5236
+ return new Promise((resolve2) => {
5237
+ d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).transform(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve2(true)), transform2);
5238
5238
  });
5239
5239
  }
5240
5240
  return false;
@@ -5336,16 +5336,16 @@ function XYPanZoom({ domNode, minZoom, maxZoom, translateExtent, viewport, onPan
5336
5336
  }
5337
5337
  async function scaleTo(zoom, options) {
5338
5338
  if (d3Selection) {
5339
- return new Promise((resolve) => {
5340
- d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleTo(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)), zoom);
5339
+ return new Promise((resolve2) => {
5340
+ d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleTo(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve2(true)), zoom);
5341
5341
  });
5342
5342
  }
5343
5343
  return false;
5344
5344
  }
5345
5345
  async function scaleBy(factor, options) {
5346
5346
  if (d3Selection) {
5347
- return new Promise((resolve) => {
5348
- d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleBy(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)), factor);
5347
+ return new Promise((resolve2) => {
5348
+ d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleBy(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve2(true)), factor);
5349
5349
  });
5350
5350
  }
5351
5351
  return false;
@@ -9133,11 +9133,11 @@ function NoteNodeInner(props) {
9133
9133
  const config = data.config ?? {};
9134
9134
  const api = useFlowEditorOptional();
9135
9135
  const hasConfigText = typeof config.text === "string" && config.text.length > 0;
9136
- const text = hasConfigText ? String(config.text) : typeof data.body === "string" ? data.body : typeof data.text === "string" ? data.text : "";
9136
+ const text3 = hasConfigText ? String(config.text) : typeof data.body === "string" ? data.body : typeof data.text === "string" ? data.text : "";
9137
9137
  const title = typeof config.title === "string" && config.title || (!hasConfigText && typeof data.body === "string" && typeof data.label === "string" ? data.label : "");
9138
9138
  const skin = NOTE_COLORS[coerceColor(config.color)];
9139
9139
  const [editing, setEditing] = ReactExports.useState(false);
9140
- const [draft, setDraft] = ReactExports.useState(text);
9140
+ const [draft, setDraft] = ReactExports.useState(text3);
9141
9141
  const areaRef = ReactExports.useRef(null);
9142
9142
  ReactExports.useEffect(() => {
9143
9143
  if (editing && areaRef.current) {
@@ -9148,7 +9148,7 @@ function NoteNodeInner(props) {
9148
9148
  }, [editing]);
9149
9149
  const commit = () => {
9150
9150
  setEditing(false);
9151
- if (!api || draft === text) return;
9151
+ if (!api || draft === text3) return;
9152
9152
  const current = api.nodes.find((n) => n.id === props.id);
9153
9153
  if (!current) return;
9154
9154
  api.updateNode({
@@ -9162,7 +9162,7 @@ function NoteNodeInner(props) {
9162
9162
  className: ["ff-note", props.selected ? "ff-note--selected" : ""].filter(Boolean).join(" "),
9163
9163
  style: { background: skin.bg, borderColor: skin.border },
9164
9164
  onDoubleClick: api ? () => {
9165
- setDraft(text);
9165
+ setDraft(text3);
9166
9166
  setEditing(true);
9167
9167
  } : void 0,
9168
9168
  children: [
@@ -9181,7 +9181,7 @@ function NoteNodeInner(props) {
9181
9181
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) commit();
9182
9182
  }
9183
9183
  }
9184
- ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-note__body", children: text || /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-note__placeholder", children: api ? "Double-click to write a note\u2026" : "Note" }) })
9184
+ ) : /* @__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" }) })
9185
9185
  ]
9186
9186
  }
9187
9187
  );
@@ -9382,6 +9382,239 @@ var RunIdentity = class _RunIdentity {
9382
9382
  }
9383
9383
  };
9384
9384
 
9385
+ // src/runtime/terminal-transcript.ts
9386
+ var ESC = String.fromCharCode(27);
9387
+ var CSI = String.fromCharCode(155);
9388
+ var BEL = String.fromCharCode(7);
9389
+ var ANSI = new RegExp(
9390
+ `[${ESC}${CSI}](?:\\[[0-?]*[ -/]*[@-~]|\\][^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)|[@-Z\\\\-_])`,
9391
+ "g"
9392
+ );
9393
+ var MAX_PENDING_ESCAPE = 1024;
9394
+ var MAX_BUFFER = 1e6;
9395
+ var TerminalTranscript = class {
9396
+ constructor() {
9397
+ /** Stripped, complete text not yet consumed by a wait. */
9398
+ this.text = "";
9399
+ /** Raw bytes held back because they may be the start of an escape sequence. */
9400
+ this.tail = "";
9401
+ this.waiters = /* @__PURE__ */ new Set();
9402
+ }
9403
+ /** Feed raw terminal output in. Safe to call with any chunking. */
9404
+ append(chunk) {
9405
+ this.tail += chunk;
9406
+ const safe = this.safeLength();
9407
+ if (safe > 0) {
9408
+ this.text += this.tail.slice(0, safe).replace(ANSI, "");
9409
+ this.tail = this.tail.slice(safe);
9410
+ }
9411
+ if (this.text.length > MAX_BUFFER) {
9412
+ this.text = this.text.slice(this.text.length - MAX_BUFFER);
9413
+ }
9414
+ for (const wake2 of [...this.waiters]) wake2();
9415
+ }
9416
+ /**
9417
+ * How much of `tail` can be stripped now.
9418
+ *
9419
+ * Everything, unless the final `ESC` has not yet been terminated — in which
9420
+ * case processing stops there and resumes when the rest arrives.
9421
+ */
9422
+ safeLength() {
9423
+ const lastEsc = Math.max(this.tail.lastIndexOf(ESC), this.tail.lastIndexOf(CSI));
9424
+ if (lastEsc === -1) return this.tail.length;
9425
+ ANSI.lastIndex = lastEsc;
9426
+ const match = ANSI.exec(this.tail);
9427
+ ANSI.lastIndex = 0;
9428
+ if (match && match.index === lastEsc) return this.tail.length;
9429
+ if (this.tail.length - lastEsc > MAX_PENDING_ESCAPE) return this.tail.length;
9430
+ return lastEsc;
9431
+ }
9432
+ /** Unconsumed output, escape sequences removed. */
9433
+ peek() {
9434
+ return this.text;
9435
+ }
9436
+ /** Drop everything currently buffered — used before typing a new command. */
9437
+ clear() {
9438
+ this.text = "";
9439
+ this.tail = "";
9440
+ }
9441
+ /**
9442
+ * Wait until `pattern` matches the unconsumed text.
9443
+ *
9444
+ * Checks what has ALREADY arrived before subscribing, because the common case
9445
+ * is that the process answered while the previous node was still finishing.
9446
+ *
9447
+ * `exited` is raced deliberately. Without it, a shell that dies reports as
9448
+ * "timed out waiting for X" — sending whoever reads it to lengthen a timeout
9449
+ * for a process that is not running. Naming the exit is the difference
9450
+ * between a diagnosis and a wrong lead.
9451
+ */
9452
+ waitFor(pattern, options) {
9453
+ return new Promise((resolve2) => {
9454
+ let settled = false;
9455
+ let timer2;
9456
+ const finish = (result) => {
9457
+ if (settled) return;
9458
+ settled = true;
9459
+ this.waiters.delete(check);
9460
+ if (timer2) clearTimeout(timer2);
9461
+ resolve2(result);
9462
+ };
9463
+ const check = () => {
9464
+ pattern.lastIndex = 0;
9465
+ const match = pattern.exec(this.text);
9466
+ if (!match) return;
9467
+ const through = match.index + (match[0].length || 1);
9468
+ const text3 = this.text.slice(0, through);
9469
+ this.text = this.text.slice(through);
9470
+ finish({ status: "matched", text: text3, match });
9471
+ };
9472
+ this.waiters.add(check);
9473
+ check();
9474
+ if (settled) return;
9475
+ if (options.timeoutMs > 0) {
9476
+ timer2 = setTimeout(() => {
9477
+ const text3 = this.text;
9478
+ this.text = "";
9479
+ finish({ status: "timeout", text: text3 });
9480
+ }, options.timeoutMs);
9481
+ timer2.unref?.();
9482
+ }
9483
+ options.exited?.then(
9484
+ (exit) => {
9485
+ if (settled) return;
9486
+ const text3 = this.text;
9487
+ this.text = "";
9488
+ finish({ status: "exited", text: text3, exitCode: exit.exitCode, signal: exit.signal });
9489
+ },
9490
+ () => {
9491
+ }
9492
+ );
9493
+ });
9494
+ }
9495
+ };
9496
+
9497
+ // src/runtime/terminal-sessions.ts
9498
+ var TerminalSessions = class {
9499
+ constructor(graph) {
9500
+ this.graph = graph;
9501
+ this.open = /* @__PURE__ */ new Map();
9502
+ this.laneOf = /* @__PURE__ */ new Map();
9503
+ this.transcripts = /* @__PURE__ */ new Map();
9504
+ this.listening = /* @__PURE__ */ new Map();
9505
+ }
9506
+ /**
9507
+ * The terminal lane a node belongs to, or null.
9508
+ *
9509
+ * Walks up `parentId` rather than checking only the immediate parent, and
9510
+ * caches per node id — a graph is walked once per run, and the answer cannot
9511
+ * change while it runs.
9512
+ */
9513
+ laneFor(nodeId, isTerminalLane) {
9514
+ const cached = this.laneOf.get(nodeId);
9515
+ if (cached !== void 0) return cached;
9516
+ const byId = new Map(this.graph.nodes.map((n) => [n.id, n]));
9517
+ const seen = /* @__PURE__ */ new Set();
9518
+ let current = byId.get(nodeId);
9519
+ let answer = null;
9520
+ while (current) {
9521
+ if (seen.has(current.id)) break;
9522
+ seen.add(current.id);
9523
+ if (current.id !== nodeId && isTerminalLane(current)) {
9524
+ answer = current.id;
9525
+ break;
9526
+ }
9527
+ const parentId = current.parentId;
9528
+ current = parentId ? byId.get(parentId) : void 0;
9529
+ }
9530
+ this.laneOf.set(nodeId, answer);
9531
+ return answer;
9532
+ }
9533
+ /**
9534
+ * The session for a lane, opening it on first use.
9535
+ *
9536
+ * The PROMISE is cached, not the resolved session. Two nodes cannot run
9537
+ * concurrently in the current engine, but a caller that awaits a
9538
+ * half-finished open would otherwise start a second process — and two shells
9539
+ * where a graph says one is the failure this class exists to prevent, arriving
9540
+ * only under concurrency and therefore only sometimes.
9541
+ */
9542
+ session(laneId, spec) {
9543
+ const existing = this.open.get(laneId);
9544
+ if (existing) return existing;
9545
+ {
9546
+ const failed = Promise.reject(
9547
+ new Error(
9548
+ "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."
9549
+ )
9550
+ );
9551
+ this.open.set(laneId, failed);
9552
+ failed.catch(() => {
9553
+ });
9554
+ return failed;
9555
+ }
9556
+ }
9557
+ /**
9558
+ * The accumulated output for a lane.
9559
+ *
9560
+ * Created on demand and kept for the run, so it exists before the session
9561
+ * resolves and survives every node that reads it. Consuming is the reader's
9562
+ * job — see `TerminalTranscript.waitFor`.
9563
+ */
9564
+ transcriptFor(laneId) {
9565
+ const existing = this.transcripts.get(laneId);
9566
+ if (existing) return existing;
9567
+ const created = new TerminalTranscript();
9568
+ this.transcripts.set(laneId, created);
9569
+ return created;
9570
+ }
9571
+ /** True once a lane has a session — used to avoid opening one during teardown. */
9572
+ isOpen(laneId) {
9573
+ return this.open.has(laneId);
9574
+ }
9575
+ /**
9576
+ * Close every session this run opened.
9577
+ *
9578
+ * Every close is attempted even if one throws: a host that fails to close one
9579
+ * PTY must not strand the others, which would leave processes alive after the
9580
+ * run reported that it had finished. Errors are returned rather than thrown,
9581
+ * because teardown runs in a `finally` and throwing there would replace the
9582
+ * run's real error with a cleanup error.
9583
+ */
9584
+ async closeAll() {
9585
+ const errors = [];
9586
+ for (const [laneId, pending] of this.open) {
9587
+ try {
9588
+ this.listening.get(laneId)?.();
9589
+ this.listening.delete(laneId);
9590
+ const session = await pending;
9591
+ await session.close();
9592
+ } catch (e) {
9593
+ errors.push(e instanceof Error ? e : new Error(`${laneId}: ${String(e)}`));
9594
+ }
9595
+ }
9596
+ this.open.clear();
9597
+ this.listening.clear();
9598
+ this.transcripts.clear();
9599
+ return errors;
9600
+ }
9601
+ };
9602
+ function specForLane(lane) {
9603
+ const config = lane.data?.config ?? {};
9604
+ const text3 = (key) => {
9605
+ const value = config[key];
9606
+ return typeof value === "string" && value !== "" ? value : void 0;
9607
+ };
9608
+ const env = config.env;
9609
+ return {
9610
+ command: text3("command"),
9611
+ cwd: text3("cwd"),
9612
+ env: env && typeof env === "object" && !Array.isArray(env) ? Object.fromEntries(
9613
+ Object.entries(env).map(([k, v]) => [k, String(v)])
9614
+ ) : void 0
9615
+ };
9616
+ }
9617
+
9385
9618
  // src/runtime/workflow-props.ts
9386
9619
  function typeOf(value) {
9387
9620
  if (value === null) return "null";
@@ -9460,6 +9693,25 @@ async function runFlow(graph, executors, onEvent = () => {
9460
9693
  const props = propsCheck.props;
9461
9694
  const declaresProps = (graph.inputs?.length ?? 0) > 0;
9462
9695
  const incomingByNode = indexIncoming(graph.edges);
9696
+ const sessions = new TerminalSessions(graph);
9697
+ const isTerminalLane = (candidate) => getNodeKind(candidate.type ?? "")?.name === "@particle-academy/terminal_lane";
9698
+ const terminalAccessorFor = (node) => {
9699
+ const laneId = sessions.laneFor(node.id, isTerminalLane);
9700
+ if (laneId === null) return void 0;
9701
+ const lane = graph.nodes.find((n) => n.id === laneId);
9702
+ if (!lane) return void 0;
9703
+ return {
9704
+ session: () => sessions.session(laneId, specForLane(lane)),
9705
+ // Awaits the session first on purpose. A transcript handed out before
9706
+ // anything is feeding it would look perfectly healthy and match nothing,
9707
+ // and "waited and nothing came" is the hardest failure here to tell apart
9708
+ // from a process that is simply slow.
9709
+ transcript: async () => {
9710
+ await sessions.session(laneId, specForLane(lane));
9711
+ return sessions.transcriptFor(laneId);
9712
+ }
9713
+ };
9714
+ };
9463
9715
  const timer2 = timeoutMs ? setTimeout(() => errors.push(`Run timed out after ${timeoutMs}ms`), timeoutMs) : null;
9464
9716
  onEvent({ type: "run-start" });
9465
9717
  try {
@@ -9525,7 +9777,8 @@ async function runFlow(graph, executors, onEvent = () => {
9525
9777
  emit: onEvent,
9526
9778
  executors,
9527
9779
  depth,
9528
- run
9780
+ run,
9781
+ terminal: terminalAccessorFor(node)
9529
9782
  })
9530
9783
  );
9531
9784
  outputs[node.id] = result;
@@ -9547,6 +9800,9 @@ async function runFlow(graph, executors, onEvent = () => {
9547
9800
  }
9548
9801
  } finally {
9549
9802
  if (timer2) clearTimeout(timer2);
9803
+ for (const error of await sessions.closeAll()) {
9804
+ onEvent({ type: "log", level: "warn", message: `terminal close failed: ${error.message}` });
9805
+ }
9550
9806
  }
9551
9807
  const ok = errors.length === 0;
9552
9808
  onEvent({ type: "run-end", ok });
@@ -9625,7 +9881,7 @@ function pickExecutor(executors, node) {
9625
9881
  for (const id2 of executorLookupIds(node)) {
9626
9882
  if (executors[id2]) return executors[id2];
9627
9883
  }
9628
- return void 0;
9884
+ return getNodeKind(node.type ?? "")?.executor;
9629
9885
  }
9630
9886
  function activatedPorts(node, result) {
9631
9887
  if (result && typeof result === "object") {
@@ -9735,6 +9991,366 @@ var subflowExecutor = async (ctx) => {
9735
9991
  return { __port: "out", value: result.outputs };
9736
9992
  };
9737
9993
 
9994
+ // src/expressions/expr.ts
9995
+ function wholeExpression(trimmed) {
9996
+ if (trimmed.length < 4) return null;
9997
+ if (!trimmed.startsWith("{{") || !trimmed.endsWith("}}")) return null;
9998
+ return trimmed.slice(2, -2);
9999
+ }
10000
+ function interpolate(template, resolve2) {
10001
+ let out = "";
10002
+ let i = 0;
10003
+ for (; ; ) {
10004
+ const open = template.indexOf("{{", i);
10005
+ if (open === -1) return out + template.slice(i);
10006
+ const close = template.indexOf("}}", open + 2);
10007
+ if (close === -1) return out + template.slice(i);
10008
+ out += template.slice(i, open) + resolve2(template.slice(open + 2, close));
10009
+ i = close + 2;
10010
+ }
10011
+ }
10012
+ var FALSY_STRINGS = /* @__PURE__ */ new Set(["", "0", "false", "no", "off", "null"]);
10013
+ var UnresolvedPathError = class extends Error {
10014
+ constructor(path) {
10015
+ super(
10016
+ `Expression path "${path}" did not resolve. Under the "throw" policy an unresolvable path is an error rather than an empty string.`
10017
+ );
10018
+ this.path = path;
10019
+ this.name = "UnresolvedPathError";
10020
+ }
10021
+ };
10022
+ function tryResolvePath(path, context) {
10023
+ const unresolved = { resolved: false, value: null };
10024
+ const trimmed = path.trim();
10025
+ if (trimmed === "") return unresolved;
10026
+ const segments = trimmed.split(".");
10027
+ let cursor;
10028
+ const head = segments[0];
10029
+ if (head === "$json" || head === "$input") {
10030
+ cursor = context !== null && typeof context === "object" && "in" in context ? context.in : context;
10031
+ segments.shift();
10032
+ } else {
10033
+ cursor = context;
10034
+ }
10035
+ for (const segment of segments) {
10036
+ if (cursor === null || cursor === void 0) return unresolved;
10037
+ if (typeof cursor !== "object") return unresolved;
10038
+ const next = cursor[segment];
10039
+ if (next === void 0) return unresolved;
10040
+ cursor = next;
10041
+ }
10042
+ return { resolved: true, value: cursor === void 0 ? null : cursor };
10043
+ }
10044
+ function evaluateExpression(template, context, options = {}) {
10045
+ if (typeof template !== "string") return template;
10046
+ const policy = options.onUnresolved ?? "empty";
10047
+ const whole = wholeExpression(template.trim());
10048
+ if (whole !== null) {
10049
+ const r = tryResolvePath(whole, context);
10050
+ if (r.resolved) return r.value;
10051
+ if (policy === "throw") throw new UnresolvedPathError(whole);
10052
+ return policy === "keep" ? template : null;
10053
+ }
10054
+ return interpolate(template, (path) => {
10055
+ const r = tryResolvePath(path, context);
10056
+ if (r.resolved) return stringify(r.value);
10057
+ if (policy === "throw") throw new UnresolvedPathError(path);
10058
+ return policy === "keep" ? `{{${path}}}` : "";
10059
+ });
10060
+ }
10061
+ function truthy(value) {
10062
+ if (typeof value === "boolean") return value;
10063
+ if (value === null || value === void 0) return false;
10064
+ if (typeof value === "string") return !FALSY_STRINGS.has(value.trim().toLowerCase());
10065
+ if (Array.isArray(value)) return value.length > 0;
10066
+ if (typeof value === "number") return value !== 0;
10067
+ return Boolean(value);
10068
+ }
10069
+ function text(value) {
10070
+ return stringify(value);
10071
+ }
10072
+ function stringify(value) {
10073
+ if (typeof value === "string") return value;
10074
+ if (typeof value === "boolean") return value ? "true" : "false";
10075
+ if (value === null || value === void 0) return "";
10076
+ if (typeof value === "number" || typeof value === "bigint") return String(value);
10077
+ try {
10078
+ return JSON.stringify(value) ?? "";
10079
+ } catch {
10080
+ return "";
10081
+ }
10082
+ }
10083
+
10084
+ // src/registry/logic.ts
10085
+ function configOf(node) {
10086
+ return node?.data?.config ?? {};
10087
+ }
10088
+ function contextOf(inputs) {
10089
+ return inputs;
10090
+ }
10091
+ function resolve(value, inputs) {
10092
+ if (typeof value !== "string") return value;
10093
+ return evaluateExpression(value, contextOf(inputs));
10094
+ }
10095
+ function conditionHolds(row, inputs) {
10096
+ const left = resolve(row.left, inputs);
10097
+ const right = resolve(row.right, inputs);
10098
+ const operator = typeof row.operator === "string" ? row.operator : "eq";
10099
+ const asText = (v) => text(v);
10100
+ const asNumber = (v) => {
10101
+ const n = typeof v === "number" ? v : Number(asText(v));
10102
+ return Number.isFinite(n) ? n : NaN;
10103
+ };
10104
+ const numeric = (fn) => {
10105
+ const a = asNumber(left);
10106
+ const b = asNumber(right);
10107
+ return Number.isFinite(a) && Number.isFinite(b) ? fn(a, b) : false;
10108
+ };
10109
+ const isEmpty = (v) => v === null || v === void 0 || v === "" || Array.isArray(v) && v.length === 0 || typeof v === "object" && !Array.isArray(v) && Object.keys(v).length === 0;
10110
+ switch (operator) {
10111
+ // Compared as TEXT, deliberately. `right` is a text field in the schema, so
10112
+ // `status == "2"` must match the number 2 an upstream node produced —
10113
+ // strict equality there would fail on a difference the author cannot see.
10114
+ case "eq":
10115
+ return asText(left) === asText(right);
10116
+ case "neq":
10117
+ return asText(left) !== asText(right);
10118
+ case "contains":
10119
+ return asText(left).includes(asText(right));
10120
+ case "not_contains":
10121
+ return !asText(left).includes(asText(right));
10122
+ case "gt":
10123
+ return numeric((a, b) => a > b);
10124
+ case "gte":
10125
+ return numeric((a, b) => a >= b);
10126
+ case "lt":
10127
+ return numeric((a, b) => a < b);
10128
+ case "lte":
10129
+ return numeric((a, b) => a <= b);
10130
+ case "truthy":
10131
+ return truthy(left);
10132
+ case "falsy":
10133
+ return !truthy(left);
10134
+ case "empty":
10135
+ return isEmpty(left);
10136
+ case "not_empty":
10137
+ return !isEmpty(left);
10138
+ default:
10139
+ return false;
10140
+ }
10141
+ }
10142
+ var branchExecutor = (ctx) => {
10143
+ const config = configOf(ctx.node);
10144
+ const inputs = ctx.inputs;
10145
+ let taken;
10146
+ const raw = config.condition;
10147
+ if (typeof raw === "string" && raw.trim() !== "") {
10148
+ taken = truthy(resolve(raw, inputs));
10149
+ } else {
10150
+ const rows = Array.isArray(config.conditions) ? config.conditions : [];
10151
+ if (rows.length === 0) {
10152
+ taken = false;
10153
+ } else {
10154
+ const results = rows.map((row) => conditionHolds(row, inputs));
10155
+ taken = config.match === "any" ? results.some(Boolean) : results.every(Boolean);
10156
+ }
10157
+ }
10158
+ return { __port: taken ? "true" : "false", value: ctx.inputs.in ?? ctx.inputs };
10159
+ };
10160
+ var transformExecutor = (ctx) => {
10161
+ const config = configOf(ctx.node);
10162
+ const inputs = ctx.inputs;
10163
+ const passthrough = ctx.inputs.in ?? ctx.inputs;
10164
+ if (config.mode === "expression") {
10165
+ const expression = config.expression;
10166
+ if (typeof expression !== "string" || expression.trim() === "") return passthrough;
10167
+ return resolve(expression, inputs);
10168
+ }
10169
+ const rows = Array.isArray(config.fields) ? config.fields : [];
10170
+ if (rows.length === 0) return passthrough;
10171
+ const out = {};
10172
+ let wrote = false;
10173
+ for (const row of rows) {
10174
+ const key = typeof row.key === "string" ? row.key.trim() : "";
10175
+ if (key === "") continue;
10176
+ out[key] = resolve(row.value, inputs);
10177
+ wrote = true;
10178
+ }
10179
+ return wrote ? out : passthrough;
10180
+ };
10181
+ var mergeExecutor = (ctx) => {
10182
+ const entries = Object.entries(ctx.inputs);
10183
+ if (configOf(ctx.node).mode === "concat") {
10184
+ const out = [];
10185
+ for (const [, value] of entries) {
10186
+ if (value === null || value === void 0) continue;
10187
+ if (Array.isArray(value)) out.push(...value);
10188
+ else out.push(value);
10189
+ }
10190
+ return out;
10191
+ }
10192
+ const merged = {};
10193
+ for (const [port, value] of entries) {
10194
+ if (value === null || value === void 0) continue;
10195
+ if (typeof value === "object" && !Array.isArray(value)) Object.assign(merged, value);
10196
+ else merged[port] = value;
10197
+ }
10198
+ return merged;
10199
+ };
10200
+ var forEachExecutor = (ctx) => {
10201
+ const source = resolve(configOf(ctx.node).source, ctx.inputs);
10202
+ let items;
10203
+ if (Array.isArray(source)) items = source;
10204
+ else if (source === null || source === void 0) items = [];
10205
+ else if (typeof source === "object") items = Object.values(source);
10206
+ else items = [source];
10207
+ return { items, count: items.length };
10208
+ };
10209
+
10210
+ // src/registry/terminal-nodes.ts
10211
+ var DEFAULT_TIMEOUT_MS = 12e4;
10212
+ var ENTER = "\r";
10213
+ function configOf2(node) {
10214
+ return node?.data?.config ?? {};
10215
+ }
10216
+ function text2(config, key) {
10217
+ const value = config[key];
10218
+ return typeof value === "string" ? value : "";
10219
+ }
10220
+ function millis(config, key, fallback) {
10221
+ const value = Number(config[key]);
10222
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
10223
+ }
10224
+ function exitMarker(nonce) {
10225
+ const token = `__fancy_flow_exit_${nonce}__`;
10226
+ return {
10227
+ token,
10228
+ // The expanded status is required to be digits. The shell ECHOES the typed
10229
+ // command back first, and that echo contains the marker with `$?`
10230
+ // unexpanded — so requiring digits is what stops the node matching its own
10231
+ // command line and reporting success before anything has run.
10232
+ pattern: new RegExp(`${token}:(\\d+)`)
10233
+ };
10234
+ }
10235
+ var counter = 0;
10236
+ function newNonce() {
10237
+ counter += 1;
10238
+ return `${Date.now().toString(36)}${counter.toString(36)}${Math.random().toString(36).slice(2, 8)}`;
10239
+ }
10240
+ function withoutMarker(output, token) {
10241
+ return output.split("\n").filter((line) => !line.includes(token)).join("\n").trim();
10242
+ }
10243
+ function fail(ctx, message) {
10244
+ ctx.abort(message);
10245
+ throw new Error(message);
10246
+ }
10247
+ function requireTerminal(ctx) {
10248
+ if (!ctx.terminal) {
10249
+ fail(
10250
+ ctx,
10251
+ `"${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.`
10252
+ );
10253
+ }
10254
+ return ctx.terminal;
10255
+ }
10256
+ function compilePattern(ctx, raw, config) {
10257
+ const flags = typeof config.flags === "string" ? config.flags.replace(/[gy]/g, "") : "";
10258
+ try {
10259
+ return config.mode === "regex" ? new RegExp(raw, flags) : new RegExp(raw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), flags);
10260
+ } catch (e) {
10261
+ return fail(ctx, `terminal_await has an invalid regex: ${e instanceof Error ? e.message : String(e)}`);
10262
+ }
10263
+ }
10264
+ var terminalRunExecutor = async (ctx) => {
10265
+ const config = configOf2(ctx.node);
10266
+ const command = text2(config, "command").trim();
10267
+ if (!command) fail(ctx, "terminal_run has no command configured");
10268
+ const terminal = requireTerminal(ctx);
10269
+ const session = await terminal.session();
10270
+ const transcript = await terminal.transcript();
10271
+ const { token, pattern } = exitMarker(newNonce());
10272
+ const timeoutMs = millis(config, "timeoutMs", DEFAULT_TIMEOUT_MS);
10273
+ transcript.clear();
10274
+ await session.write(`${command}; printf '${token}:%s\\n' "$?"${ENTER}`);
10275
+ const result = await transcript.waitFor(pattern, { timeoutMs, exited: session.exited });
10276
+ if (result.status === "exited") {
10277
+ fail(
10278
+ ctx,
10279
+ `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.`
10280
+ );
10281
+ }
10282
+ if (result.status === "timeout") {
10283
+ fail(
10284
+ ctx,
10285
+ `"${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.`
10286
+ );
10287
+ }
10288
+ const exitCode = Number(result.match[1]);
10289
+ const output = withoutMarker(result.text, token);
10290
+ const failOnNonZero = config.failOnNonZero !== false;
10291
+ if (failOnNonZero && exitCode !== 0) {
10292
+ ctx.abort(`"${command}" exited ${exitCode}.
10293
+ ${output}`);
10294
+ }
10295
+ return { output, exitCode, command };
10296
+ };
10297
+ var terminalSendExecutor = async (ctx) => {
10298
+ const config = configOf2(ctx.node);
10299
+ const terminal = requireTerminal(ctx);
10300
+ const session = await terminal.session();
10301
+ const body = text2(config, "text");
10302
+ const submit = config.submit !== false;
10303
+ if (config.clearFirst === true) {
10304
+ (await terminal.transcript()).clear();
10305
+ }
10306
+ await session.write(submit ? `${body}${ENTER}` : body);
10307
+ return { sent: body, submitted: submit };
10308
+ };
10309
+ var terminalAwaitExecutor = async (ctx) => {
10310
+ const config = configOf2(ctx.node);
10311
+ const terminal = requireTerminal(ctx);
10312
+ const session = await terminal.session();
10313
+ const transcript = await terminal.transcript();
10314
+ const raw = text2(config, "pattern").trim();
10315
+ if (!raw) fail(ctx, "terminal_await has no pattern configured \u2014 there is nothing for it to wait for");
10316
+ const pattern = compilePattern(ctx, raw, config);
10317
+ const timeoutMs = millis(config, "timeoutMs", DEFAULT_TIMEOUT_MS);
10318
+ const result = await transcript.waitFor(pattern, {
10319
+ timeoutMs,
10320
+ exited: session.exited
10321
+ });
10322
+ if (result.status === "exited") {
10323
+ fail(
10324
+ ctx,
10325
+ `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.`
10326
+ );
10327
+ }
10328
+ if (result.status === "timeout") {
10329
+ if (config.onTimeout === "continue") {
10330
+ ctx.emit({
10331
+ type: "log",
10332
+ nodeId: ctx.node.id,
10333
+ level: "warn",
10334
+ message: `terminal_await: ${JSON.stringify(raw)} did not appear within ${timeoutMs}ms; continuing as configured.`
10335
+ });
10336
+ return { matched: false, output: result.text, groups: [] };
10337
+ }
10338
+ fail(
10339
+ ctx,
10340
+ `${JSON.stringify(raw)} did not appear within ${timeoutMs}ms. Last output:
10341
+ ${result.text.slice(-2e3)}`
10342
+ );
10343
+ }
10344
+ return {
10345
+ matched: true,
10346
+ output: result.text,
10347
+ matchedText: result.match[0],
10348
+ // Capture groups are the reason to use regex mode at all — a prompt that
10349
+ // reports a session id or a file path is only useful if the value comes out.
10350
+ groups: result.match.slice(1)
10351
+ };
10352
+ };
10353
+
9738
10354
  // src/registry/builtin.ts
9739
10355
  var HUMAN_FIELD_OUTPUT_TYPE = {
9740
10356
  text: "string",
@@ -10024,6 +10640,9 @@ var KINDS = [
10024
10640
  // returns its input on the chosen port
10025
10641
  emits: "input",
10026
10642
  aliases: ["branch", "@fancy/branch"],
10643
+ // Pure logic: no I/O to choose, so a default is safe. A host
10644
+ // executor still wins -- pickExecutor consults the registry first.
10645
+ executor: branchExecutor,
10027
10646
  category: "logic",
10028
10647
  label: "Branch",
10029
10648
  description: "Multi-way branch on a condition or value.",
@@ -10121,6 +10740,9 @@ var KINDS = [
10121
10740
  { path: "count", type: "number" }
10122
10741
  ],
10123
10742
  aliases: ["for_each", "@fancy/for_each"],
10743
+ // Pure logic: no I/O to choose, so a default is safe. A host
10744
+ // executor still wins -- pickExecutor consults the registry first.
10745
+ executor: forEachExecutor,
10124
10746
  category: "logic",
10125
10747
  label: "For Each",
10126
10748
  description: "Iterate over a list, emitting each item on `item`.",
@@ -10141,6 +10763,9 @@ var KINDS = [
10141
10763
  // reference.
10142
10764
  emits: (config) => (config?.mode ?? "merge") === "concat" ? null : "inputs-merged",
10143
10765
  aliases: ["merge", "@fancy/merge"],
10766
+ // Pure logic: no I/O to choose, so a default is safe. A host
10767
+ // executor still wins -- pickExecutor consults the registry first.
10768
+ executor: mergeExecutor,
10144
10769
  category: "logic",
10145
10770
  label: "Merge",
10146
10771
  description: "Combine multiple inputs into one object or array.",
@@ -10186,6 +10811,9 @@ var KINDS = [
10186
10811
  // else the shape that expression names. So the RELATION is config-dependent.
10187
10812
  emits: (config) => (config?.expression ?? "") === "" ? "input" : "expression:expression",
10188
10813
  aliases: ["transform", "@fancy/transform"],
10814
+ // Pure logic: no I/O to choose, so a default is safe. A host
10815
+ // executor still wins -- pickExecutor consults the registry first.
10816
+ executor: transformExecutor,
10189
10817
  category: "logic",
10190
10818
  label: "Transform",
10191
10819
  description: "Reshape data with an expression.",
@@ -10644,6 +11272,179 @@ var KINDS = [
10644
11272
  resizable: { minWidth: 160, minHeight: 72 },
10645
11273
  component: LaneNode
10646
11274
  },
11275
+ {
11276
+ // Terminal lane — a lane that OWNS a terminal for the length of the run.
11277
+ //
11278
+ // Visually a swimlane, and `layout` for the same reason the plain lane is:
11279
+ // a lane is not a step, so it never executes and never appears in topo
11280
+ // order. What is new is that it DECLARES a resource. The runtime opens its
11281
+ // terminal lazily, when the first terminal node inside it runs, and closes
11282
+ // it in the run's `finally` — so a graph that never reaches a terminal node
11283
+ // never spawns a process, and one that does gets exactly one session no
11284
+ // matter how many nodes use it.
11285
+ //
11286
+ // Membership is `parentId`, which already persists into the WorkflowSchema.
11287
+ // That is what lets a headless runtime resolve the same grouping the canvas
11288
+ // shows, without a second association to keep in sync.
11289
+ name: "@particle-academy/terminal_lane",
11290
+ aliases: ["terminal_lane", "@fancy/terminal_lane"],
11291
+ category: "layout",
11292
+ label: "Terminal lane",
11293
+ description: "A lane that owns one terminal. Opens at the first terminal node, closes when the run ends.",
11294
+ icon: "\u25A3",
11295
+ inputs: [],
11296
+ outputs: [],
11297
+ configSchema: [
11298
+ { type: "text", key: "title", label: "Title", default: "Terminal" },
11299
+ {
11300
+ type: "text",
11301
+ key: "command",
11302
+ label: "Command",
11303
+ placeholder: "Leave empty for the default shell"
11304
+ },
11305
+ { type: "text", key: "cwd", label: "Working directory", placeholder: "Host default" },
11306
+ { type: "keyvalue", key: "env", label: "Environment" },
11307
+ {
11308
+ type: "select",
11309
+ key: "orientation",
11310
+ label: "Orientation",
11311
+ default: "horizontal",
11312
+ options: [
11313
+ { value: "horizontal", label: "Row" },
11314
+ { value: "vertical", label: "Column" }
11315
+ ]
11316
+ }
11317
+ ],
11318
+ defaultConfig: { title: "Terminal", orientation: "horizontal" },
11319
+ resizable: { minWidth: 220, minHeight: 120 },
11320
+ component: LaneNode
11321
+ },
11322
+ {
11323
+ // Terminal nodes. All three live inside a terminal lane and talk to the
11324
+ // session that lane owns; outside one they abort by name rather than
11325
+ // opening a shell of their own.
11326
+ name: "@particle-academy/terminal_run",
11327
+ aliases: ["terminal_run", "@fancy/terminal_run"],
11328
+ category: "io",
11329
+ label: "Run in terminal",
11330
+ description: "Run a shell command in the lane's terminal and wait for its exit code.",
11331
+ icon: "$",
11332
+ inputs: [{ id: "in" }],
11333
+ outputs: [{ id: "out", label: "output" }],
11334
+ executor: terminalRunExecutor,
11335
+ configSchema: [
11336
+ {
11337
+ type: "textarea",
11338
+ key: "command",
11339
+ label: "Command",
11340
+ rows: 3,
11341
+ required: true,
11342
+ placeholder: "npm test",
11343
+ 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."
11344
+ },
11345
+ {
11346
+ type: "number",
11347
+ key: "timeoutMs",
11348
+ label: "Timeout (ms)",
11349
+ default: 12e4,
11350
+ description: "How long to wait for the command to finish before failing the run."
11351
+ },
11352
+ {
11353
+ type: "switch",
11354
+ key: "failOnNonZero",
11355
+ label: "Fail the run on a non-zero exit",
11356
+ default: true,
11357
+ 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."
11358
+ }
11359
+ ],
11360
+ defaultConfig: { command: "", timeoutMs: 12e4, failOnNonZero: true }
11361
+ },
11362
+ {
11363
+ name: "@particle-academy/terminal_send",
11364
+ aliases: ["terminal_send", "@fancy/terminal_send"],
11365
+ category: "io",
11366
+ label: "Send to terminal",
11367
+ description: "Type text at whatever is running in the lane's terminal, without waiting.",
11368
+ icon: "\u2328",
11369
+ inputs: [{ id: "in" }],
11370
+ outputs: [{ id: "out", label: "sent" }],
11371
+ executor: terminalSendExecutor,
11372
+ configSchema: [
11373
+ {
11374
+ type: "textarea",
11375
+ key: "text",
11376
+ label: "Text",
11377
+ rows: 4,
11378
+ placeholder: "Summarise the failing test and propose a fix.",
11379
+ description: "What to type. This is how a graph prompts an agent TUI such as Claude Code or Codex."
11380
+ },
11381
+ {
11382
+ type: "switch",
11383
+ key: "submit",
11384
+ label: "Press Enter",
11385
+ default: true,
11386
+ description: "Off leaves the text on the input line \u2014 useful for building one up across several nodes."
11387
+ },
11388
+ {
11389
+ type: "switch",
11390
+ key: "clearFirst",
11391
+ label: "Forget earlier output first",
11392
+ default: false,
11393
+ description: "Discards anything the terminal said before this send, so a following Await cannot match a prompt left over from the previous exchange."
11394
+ }
11395
+ ],
11396
+ defaultConfig: { text: "", submit: true, clearFirst: false }
11397
+ },
11398
+ {
11399
+ name: "@particle-academy/terminal_await",
11400
+ aliases: ["terminal_await", "@fancy/terminal_await"],
11401
+ category: "io",
11402
+ label: "Await terminal output",
11403
+ description: "Wait until the lane's terminal prints something that matches.",
11404
+ icon: "\u23F1",
11405
+ inputs: [{ id: "in" }],
11406
+ outputs: [{ id: "out", label: "output" }],
11407
+ executor: terminalAwaitExecutor,
11408
+ configSchema: [
11409
+ {
11410
+ type: "text",
11411
+ key: "pattern",
11412
+ label: "Wait for",
11413
+ required: true,
11414
+ placeholder: "esc to interrupt",
11415
+ description: "Matched against the output with colour codes already stripped, so match what you SEE."
11416
+ },
11417
+ {
11418
+ type: "select",
11419
+ key: "mode",
11420
+ label: "Match as",
11421
+ default: "text",
11422
+ options: [
11423
+ { value: "text", label: "Plain text" },
11424
+ { value: "regex", label: "Regular expression" }
11425
+ ],
11426
+ description: "Regex mode also returns capture groups, which is how a value gets out of a prompt."
11427
+ },
11428
+ {
11429
+ type: "number",
11430
+ key: "timeoutMs",
11431
+ label: "Timeout (ms)",
11432
+ default: 12e4
11433
+ },
11434
+ {
11435
+ type: "select",
11436
+ key: "onTimeout",
11437
+ label: "If it never appears",
11438
+ default: "fail",
11439
+ options: [
11440
+ { value: "fail", label: "Fail the run" },
11441
+ { value: "continue", label: "Continue with matched: false" }
11442
+ ],
11443
+ 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."
11444
+ }
11445
+ ],
11446
+ defaultConfig: { pattern: "", mode: "text", timeoutMs: 12e4, onTimeout: "fail" }
11447
+ },
10647
11448
  {
10648
11449
  // Note — a sticky-note annotation. Portless + visual-only: the runtime skips
10649
11450
  // the `annotation` category, so a note's text NEVER reaches a runner — it
@@ -10931,17 +11732,6 @@ function pauseForHuman(ctx, awaiting, detail) {
10931
11732
  return ctx.abort(encodePause({ nodeId: ctx.node.id, awaiting, detail }));
10932
11733
  }
10933
11734
 
10934
- // src/expressions/expr.ts
10935
- var FALSY_STRINGS = /* @__PURE__ */ new Set(["", "0", "false", "no", "off", "null"]);
10936
- function truthy(value) {
10937
- if (typeof value === "boolean") return value;
10938
- if (value === null || value === void 0) return false;
10939
- if (typeof value === "string") return !FALSY_STRINGS.has(value.trim().toLowerCase());
10940
- if (Array.isArray(value)) return value.length > 0;
10941
- if (typeof value === "number") return value !== 0;
10942
- return Boolean(value);
10943
- }
10944
-
10945
11735
  // src/durable/human.ts
10946
11736
  var NotAwaitingHuman = class extends Error {
10947
11737
  constructor(message) {