@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.
Files changed (105) 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 +837 -36
  91. package/dist/ux.cjs.map +1 -1
  92. package/dist/ux.d.cts +2 -2
  93. package/dist/ux.d.ts +2 -2
  94. package/dist/ux.js +2 -2
  95. package/package.json +11 -1
  96. package/dist/capabilities-BYa5p5jw.d.cts +0 -112
  97. package/dist/capabilities-COOXRiNL.d.ts +0 -112
  98. package/dist/chunk-JF6WCRBU.js.map +0 -1
  99. package/dist/chunk-UM4C46AF.js +0 -103
  100. package/dist/chunk-UM4C46AF.js.map +0 -1
  101. package/dist/chunk-USL4FMFU.js +0 -41
  102. package/dist/chunk-USL4FMFU.js.map +0 -1
  103. package/dist/chunk-W5DPKJY4.js.map +0 -1
  104. package/dist/types-JFYjPJAG.d.cts +0 -333
  105. package/dist/types-JFYjPJAG.d.ts +0 -333
package/dist/ux.cjs CHANGED
@@ -2167,38 +2167,38 @@ function attrRemoveNS2(fullname) {
2167
2167
  this.removeAttributeNS(fullname.space, fullname.local);
2168
2168
  };
2169
2169
  }
2170
- function attrConstant2(name, interpolate, value1) {
2170
+ function attrConstant2(name, interpolate2, value1) {
2171
2171
  var string00, string1 = value1 + "", interpolate0;
2172
2172
  return function() {
2173
2173
  var string0 = this.getAttribute(name);
2174
- return string0 === string1 ? null : string0 === string00 ? interpolate0 : interpolate0 = interpolate(string00 = string0, value1);
2174
+ return string0 === string1 ? null : string0 === string00 ? interpolate0 : interpolate0 = interpolate2(string00 = string0, value1);
2175
2175
  };
2176
2176
  }
2177
- function attrConstantNS2(fullname, interpolate, value1) {
2177
+ function attrConstantNS2(fullname, interpolate2, value1) {
2178
2178
  var string00, string1 = value1 + "", interpolate0;
2179
2179
  return function() {
2180
2180
  var string0 = this.getAttributeNS(fullname.space, fullname.local);
2181
- return string0 === string1 ? null : string0 === string00 ? interpolate0 : interpolate0 = interpolate(string00 = string0, value1);
2181
+ return string0 === string1 ? null : string0 === string00 ? interpolate0 : interpolate0 = interpolate2(string00 = string0, value1);
2182
2182
  };
2183
2183
  }
2184
- function attrFunction2(name, interpolate, value) {
2184
+ function attrFunction2(name, interpolate2, value) {
2185
2185
  var string00, string10, interpolate0;
2186
2186
  return function() {
2187
2187
  var string0, value1 = value(this), string1;
2188
2188
  if (value1 == null) return void this.removeAttribute(name);
2189
2189
  string0 = this.getAttribute(name);
2190
2190
  string1 = value1 + "";
2191
- return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));
2191
+ return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : (string10 = string1, interpolate0 = interpolate2(string00 = string0, value1));
2192
2192
  };
2193
2193
  }
2194
- function attrFunctionNS2(fullname, interpolate, value) {
2194
+ function attrFunctionNS2(fullname, interpolate2, value) {
2195
2195
  var string00, string10, interpolate0;
2196
2196
  return function() {
2197
2197
  var string0, value1 = value(this), string1;
2198
2198
  if (value1 == null) return void this.removeAttributeNS(fullname.space, fullname.local);
2199
2199
  string0 = this.getAttributeNS(fullname.space, fullname.local);
2200
2200
  string1 = value1 + "";
2201
- return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));
2201
+ return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : (string10 = string1, interpolate0 = interpolate2(string00 = string0, value1));
2202
2202
  };
2203
2203
  }
2204
2204
  function attr_default2(name, value) {
@@ -2408,11 +2408,11 @@ function selection_default2() {
2408
2408
  }
2409
2409
 
2410
2410
  // node_modules/d3-transition/src/transition/style.js
2411
- function styleNull(name, interpolate) {
2411
+ function styleNull(name, interpolate2) {
2412
2412
  var string00, string10, interpolate0;
2413
2413
  return function() {
2414
2414
  var string0 = styleValue(this, name), string1 = (this.style.removeProperty(name), styleValue(this, name));
2415
- return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : interpolate0 = interpolate(string00 = string0, string10 = string1);
2415
+ return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : interpolate0 = interpolate2(string00 = string0, string10 = string1);
2416
2416
  };
2417
2417
  }
2418
2418
  function styleRemove2(name) {
@@ -2420,19 +2420,19 @@ function styleRemove2(name) {
2420
2420
  this.style.removeProperty(name);
2421
2421
  };
2422
2422
  }
2423
- function styleConstant2(name, interpolate, value1) {
2423
+ function styleConstant2(name, interpolate2, value1) {
2424
2424
  var string00, string1 = value1 + "", interpolate0;
2425
2425
  return function() {
2426
2426
  var string0 = styleValue(this, name);
2427
- return string0 === string1 ? null : string0 === string00 ? interpolate0 : interpolate0 = interpolate(string00 = string0, value1);
2427
+ return string0 === string1 ? null : string0 === string00 ? interpolate0 : interpolate0 = interpolate2(string00 = string0, value1);
2428
2428
  };
2429
2429
  }
2430
- function styleFunction2(name, interpolate, value) {
2430
+ function styleFunction2(name, interpolate2, value) {
2431
2431
  var string00, string10, interpolate0;
2432
2432
  return function() {
2433
2433
  var string0 = styleValue(this, name), value1 = value(this), string1 = value1 + "";
2434
2434
  if (value1 == null) string1 = value1 = (this.style.removeProperty(name), styleValue(this, name));
2435
- return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));
2435
+ return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : (string10 = string1, interpolate0 = interpolate2(string00 = string0, value1));
2436
2436
  };
2437
2437
  }
2438
2438
  function styleMaybeRemove(id2, name) {
@@ -2534,9 +2534,9 @@ function transition_default() {
2534
2534
  // node_modules/d3-transition/src/transition/end.js
2535
2535
  function end_default() {
2536
2536
  var on0, on1, that = this, id2 = that._id, size = that.size();
2537
- return new Promise(function(resolve, reject) {
2537
+ return new Promise(function(resolve2, reject) {
2538
2538
  var cancel = { value: reject }, end = { value: function() {
2539
- if (--size === 0) resolve();
2539
+ if (--size === 0) resolve2();
2540
2540
  } };
2541
2541
  that.each(function() {
2542
2542
  var schedule = set2(this, id2), on = schedule.on;
@@ -2548,7 +2548,7 @@ function end_default() {
2548
2548
  }
2549
2549
  schedule.on = on1;
2550
2550
  });
2551
- if (size === 0) resolve();
2551
+ if (size === 0) resolve2();
2552
2552
  });
2553
2553
  }
2554
2554
 
@@ -2750,7 +2750,7 @@ function defaultConstrain(transform2, extent, translateExtent) {
2750
2750
  );
2751
2751
  }
2752
2752
  function zoom_default2() {
2753
- 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;
2753
+ 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;
2754
2754
  function zoom(selection2) {
2755
2755
  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)");
2756
2756
  }
@@ -2811,7 +2811,7 @@ function zoom_default2() {
2811
2811
  }).on("interrupt.zoom end.zoom", function() {
2812
2812
  gesture(this, arguments).event(event).end();
2813
2813
  }).tween("zoom", function() {
2814
- 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));
2814
+ 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));
2815
2815
  return function(t) {
2816
2816
  if (t === 1) t = b;
2817
2817
  else {
@@ -3016,7 +3016,7 @@ function zoom_default2() {
3016
3016
  return arguments.length ? (duration = +_, zoom) : duration;
3017
3017
  };
3018
3018
  zoom.interpolate = function(_) {
3019
- return arguments.length ? (interpolate = _, zoom) : interpolate;
3019
+ return arguments.length ? (interpolate2 = _, zoom) : interpolate2;
3020
3020
  };
3021
3021
  zoom.on = function() {
3022
3022
  var value = listeners2.on.apply(listeners2, arguments);
@@ -3509,13 +3509,13 @@ function areSetsEqual(a, b) {
3509
3509
  return true;
3510
3510
  }
3511
3511
  function withResolvers() {
3512
- let resolve;
3512
+ let resolve2;
3513
3513
  let reject;
3514
3514
  const promise = new Promise((res, rej) => {
3515
- resolve = res;
3515
+ resolve2 = res;
3516
3516
  reject = rej;
3517
3517
  });
3518
- return { promise, resolve, reject };
3518
+ return { promise, resolve: resolve2, reject };
3519
3519
  }
3520
3520
  function mergeAriaLabelConfig(partial) {
3521
3521
  return { ...defaultAriaLabelConfig, ...partial || {} };
@@ -5131,8 +5131,8 @@ function XYPanZoom({ domNode, minZoom, maxZoom, translateExtent, viewport, onPan
5131
5131
  d3ZoomInstance.wheelDelta(wheelDelta);
5132
5132
  async function setTransform(transform2, options) {
5133
5133
  if (d3Selection) {
5134
- return new Promise((resolve) => {
5135
- d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).transform(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)), transform2);
5134
+ return new Promise((resolve2) => {
5135
+ d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).transform(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve2(true)), transform2);
5136
5136
  });
5137
5137
  }
5138
5138
  return false;
@@ -5234,16 +5234,16 @@ function XYPanZoom({ domNode, minZoom, maxZoom, translateExtent, viewport, onPan
5234
5234
  }
5235
5235
  async function scaleTo(zoom, options) {
5236
5236
  if (d3Selection) {
5237
- return new Promise((resolve) => {
5238
- d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleTo(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)), zoom);
5237
+ return new Promise((resolve2) => {
5238
+ d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleTo(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve2(true)), zoom);
5239
5239
  });
5240
5240
  }
5241
5241
  return false;
5242
5242
  }
5243
5243
  async function scaleBy(factor, options) {
5244
5244
  if (d3Selection) {
5245
- return new Promise((resolve) => {
5246
- d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleBy(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)), factor);
5245
+ return new Promise((resolve2) => {
5246
+ d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleBy(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve2(true)), factor);
5247
5247
  });
5248
5248
  }
5249
5249
  return false;
@@ -9031,11 +9031,11 @@ function NoteNodeInner(props) {
9031
9031
  const config = data.config ?? {};
9032
9032
  const api = useFlowEditorOptional();
9033
9033
  const hasConfigText = typeof config.text === "string" && config.text.length > 0;
9034
- const text = hasConfigText ? String(config.text) : typeof data.body === "string" ? data.body : typeof data.text === "string" ? data.text : "";
9034
+ const text3 = hasConfigText ? String(config.text) : typeof data.body === "string" ? data.body : typeof data.text === "string" ? data.text : "";
9035
9035
  const title = typeof config.title === "string" && config.title || (!hasConfigText && typeof data.body === "string" && typeof data.label === "string" ? data.label : "");
9036
9036
  const skin = NOTE_COLORS[coerceColor(config.color)];
9037
9037
  const [editing, setEditing] = ReactExports.useState(false);
9038
- const [draft, setDraft] = ReactExports.useState(text);
9038
+ const [draft, setDraft] = ReactExports.useState(text3);
9039
9039
  const areaRef = ReactExports.useRef(null);
9040
9040
  ReactExports.useEffect(() => {
9041
9041
  if (editing && areaRef.current) {
@@ -9046,7 +9046,7 @@ function NoteNodeInner(props) {
9046
9046
  }, [editing]);
9047
9047
  const commit = () => {
9048
9048
  setEditing(false);
9049
- if (!api || draft === text) return;
9049
+ if (!api || draft === text3) return;
9050
9050
  const current = api.nodes.find((n) => n.id === props.id);
9051
9051
  if (!current) return;
9052
9052
  api.updateNode({
@@ -9060,7 +9060,7 @@ function NoteNodeInner(props) {
9060
9060
  className: ["ff-note", props.selected ? "ff-note--selected" : ""].filter(Boolean).join(" "),
9061
9061
  style: { background: skin.bg, borderColor: skin.border },
9062
9062
  onDoubleClick: api ? () => {
9063
- setDraft(text);
9063
+ setDraft(text3);
9064
9064
  setEditing(true);
9065
9065
  } : void 0,
9066
9066
  children: [
@@ -9079,7 +9079,7 @@ function NoteNodeInner(props) {
9079
9079
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) commit();
9080
9080
  }
9081
9081
  }
9082
- ) : /* @__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" }) })
9082
+ ) : /* @__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" }) })
9083
9083
  ]
9084
9084
  }
9085
9085
  );
@@ -9280,6 +9280,239 @@ var RunIdentity = class _RunIdentity {
9280
9280
  }
9281
9281
  };
9282
9282
 
9283
+ // src/runtime/terminal-transcript.ts
9284
+ var ESC = String.fromCharCode(27);
9285
+ var CSI = String.fromCharCode(155);
9286
+ var BEL = String.fromCharCode(7);
9287
+ var ANSI = new RegExp(
9288
+ `[${ESC}${CSI}](?:\\[[0-?]*[ -/]*[@-~]|\\][^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)|[@-Z\\\\-_])`,
9289
+ "g"
9290
+ );
9291
+ var MAX_PENDING_ESCAPE = 1024;
9292
+ var MAX_BUFFER = 1e6;
9293
+ var TerminalTranscript = class {
9294
+ constructor() {
9295
+ /** Stripped, complete text not yet consumed by a wait. */
9296
+ this.text = "";
9297
+ /** Raw bytes held back because they may be the start of an escape sequence. */
9298
+ this.tail = "";
9299
+ this.waiters = /* @__PURE__ */ new Set();
9300
+ }
9301
+ /** Feed raw terminal output in. Safe to call with any chunking. */
9302
+ append(chunk) {
9303
+ this.tail += chunk;
9304
+ const safe = this.safeLength();
9305
+ if (safe > 0) {
9306
+ this.text += this.tail.slice(0, safe).replace(ANSI, "");
9307
+ this.tail = this.tail.slice(safe);
9308
+ }
9309
+ if (this.text.length > MAX_BUFFER) {
9310
+ this.text = this.text.slice(this.text.length - MAX_BUFFER);
9311
+ }
9312
+ for (const wake2 of [...this.waiters]) wake2();
9313
+ }
9314
+ /**
9315
+ * How much of `tail` can be stripped now.
9316
+ *
9317
+ * Everything, unless the final `ESC` has not yet been terminated — in which
9318
+ * case processing stops there and resumes when the rest arrives.
9319
+ */
9320
+ safeLength() {
9321
+ const lastEsc = Math.max(this.tail.lastIndexOf(ESC), this.tail.lastIndexOf(CSI));
9322
+ if (lastEsc === -1) return this.tail.length;
9323
+ ANSI.lastIndex = lastEsc;
9324
+ const match = ANSI.exec(this.tail);
9325
+ ANSI.lastIndex = 0;
9326
+ if (match && match.index === lastEsc) return this.tail.length;
9327
+ if (this.tail.length - lastEsc > MAX_PENDING_ESCAPE) return this.tail.length;
9328
+ return lastEsc;
9329
+ }
9330
+ /** Unconsumed output, escape sequences removed. */
9331
+ peek() {
9332
+ return this.text;
9333
+ }
9334
+ /** Drop everything currently buffered — used before typing a new command. */
9335
+ clear() {
9336
+ this.text = "";
9337
+ this.tail = "";
9338
+ }
9339
+ /**
9340
+ * Wait until `pattern` matches the unconsumed text.
9341
+ *
9342
+ * Checks what has ALREADY arrived before subscribing, because the common case
9343
+ * is that the process answered while the previous node was still finishing.
9344
+ *
9345
+ * `exited` is raced deliberately. Without it, a shell that dies reports as
9346
+ * "timed out waiting for X" — sending whoever reads it to lengthen a timeout
9347
+ * for a process that is not running. Naming the exit is the difference
9348
+ * between a diagnosis and a wrong lead.
9349
+ */
9350
+ waitFor(pattern, options) {
9351
+ return new Promise((resolve2) => {
9352
+ let settled = false;
9353
+ let timer2;
9354
+ const finish = (result) => {
9355
+ if (settled) return;
9356
+ settled = true;
9357
+ this.waiters.delete(check);
9358
+ if (timer2) clearTimeout(timer2);
9359
+ resolve2(result);
9360
+ };
9361
+ const check = () => {
9362
+ pattern.lastIndex = 0;
9363
+ const match = pattern.exec(this.text);
9364
+ if (!match) return;
9365
+ const through = match.index + (match[0].length || 1);
9366
+ const text3 = this.text.slice(0, through);
9367
+ this.text = this.text.slice(through);
9368
+ finish({ status: "matched", text: text3, match });
9369
+ };
9370
+ this.waiters.add(check);
9371
+ check();
9372
+ if (settled) return;
9373
+ if (options.timeoutMs > 0) {
9374
+ timer2 = setTimeout(() => {
9375
+ const text3 = this.text;
9376
+ this.text = "";
9377
+ finish({ status: "timeout", text: text3 });
9378
+ }, options.timeoutMs);
9379
+ timer2.unref?.();
9380
+ }
9381
+ options.exited?.then(
9382
+ (exit) => {
9383
+ if (settled) return;
9384
+ const text3 = this.text;
9385
+ this.text = "";
9386
+ finish({ status: "exited", text: text3, exitCode: exit.exitCode, signal: exit.signal });
9387
+ },
9388
+ () => {
9389
+ }
9390
+ );
9391
+ });
9392
+ }
9393
+ };
9394
+
9395
+ // src/runtime/terminal-sessions.ts
9396
+ var TerminalSessions = class {
9397
+ constructor(graph) {
9398
+ this.graph = graph;
9399
+ this.open = /* @__PURE__ */ new Map();
9400
+ this.laneOf = /* @__PURE__ */ new Map();
9401
+ this.transcripts = /* @__PURE__ */ new Map();
9402
+ this.listening = /* @__PURE__ */ new Map();
9403
+ }
9404
+ /**
9405
+ * The terminal lane a node belongs to, or null.
9406
+ *
9407
+ * Walks up `parentId` rather than checking only the immediate parent, and
9408
+ * caches per node id — a graph is walked once per run, and the answer cannot
9409
+ * change while it runs.
9410
+ */
9411
+ laneFor(nodeId, isTerminalLane) {
9412
+ const cached = this.laneOf.get(nodeId);
9413
+ if (cached !== void 0) return cached;
9414
+ const byId = new Map(this.graph.nodes.map((n) => [n.id, n]));
9415
+ const seen = /* @__PURE__ */ new Set();
9416
+ let current = byId.get(nodeId);
9417
+ let answer = null;
9418
+ while (current) {
9419
+ if (seen.has(current.id)) break;
9420
+ seen.add(current.id);
9421
+ if (current.id !== nodeId && isTerminalLane(current)) {
9422
+ answer = current.id;
9423
+ break;
9424
+ }
9425
+ const parentId = current.parentId;
9426
+ current = parentId ? byId.get(parentId) : void 0;
9427
+ }
9428
+ this.laneOf.set(nodeId, answer);
9429
+ return answer;
9430
+ }
9431
+ /**
9432
+ * The session for a lane, opening it on first use.
9433
+ *
9434
+ * The PROMISE is cached, not the resolved session. Two nodes cannot run
9435
+ * concurrently in the current engine, but a caller that awaits a
9436
+ * half-finished open would otherwise start a second process — and two shells
9437
+ * where a graph says one is the failure this class exists to prevent, arriving
9438
+ * only under concurrency and therefore only sometimes.
9439
+ */
9440
+ session(laneId, spec) {
9441
+ const existing = this.open.get(laneId);
9442
+ if (existing) return existing;
9443
+ {
9444
+ const failed = Promise.reject(
9445
+ new Error(
9446
+ "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."
9447
+ )
9448
+ );
9449
+ this.open.set(laneId, failed);
9450
+ failed.catch(() => {
9451
+ });
9452
+ return failed;
9453
+ }
9454
+ }
9455
+ /**
9456
+ * The accumulated output for a lane.
9457
+ *
9458
+ * Created on demand and kept for the run, so it exists before the session
9459
+ * resolves and survives every node that reads it. Consuming is the reader's
9460
+ * job — see `TerminalTranscript.waitFor`.
9461
+ */
9462
+ transcriptFor(laneId) {
9463
+ const existing = this.transcripts.get(laneId);
9464
+ if (existing) return existing;
9465
+ const created = new TerminalTranscript();
9466
+ this.transcripts.set(laneId, created);
9467
+ return created;
9468
+ }
9469
+ /** True once a lane has a session — used to avoid opening one during teardown. */
9470
+ isOpen(laneId) {
9471
+ return this.open.has(laneId);
9472
+ }
9473
+ /**
9474
+ * Close every session this run opened.
9475
+ *
9476
+ * Every close is attempted even if one throws: a host that fails to close one
9477
+ * PTY must not strand the others, which would leave processes alive after the
9478
+ * run reported that it had finished. Errors are returned rather than thrown,
9479
+ * because teardown runs in a `finally` and throwing there would replace the
9480
+ * run's real error with a cleanup error.
9481
+ */
9482
+ async closeAll() {
9483
+ const errors = [];
9484
+ for (const [laneId, pending] of this.open) {
9485
+ try {
9486
+ this.listening.get(laneId)?.();
9487
+ this.listening.delete(laneId);
9488
+ const session = await pending;
9489
+ await session.close();
9490
+ } catch (e) {
9491
+ errors.push(e instanceof Error ? e : new Error(`${laneId}: ${String(e)}`));
9492
+ }
9493
+ }
9494
+ this.open.clear();
9495
+ this.listening.clear();
9496
+ this.transcripts.clear();
9497
+ return errors;
9498
+ }
9499
+ };
9500
+ function specForLane(lane) {
9501
+ const config = lane.data?.config ?? {};
9502
+ const text3 = (key) => {
9503
+ const value = config[key];
9504
+ return typeof value === "string" && value !== "" ? value : void 0;
9505
+ };
9506
+ const env = config.env;
9507
+ return {
9508
+ command: text3("command"),
9509
+ cwd: text3("cwd"),
9510
+ env: env && typeof env === "object" && !Array.isArray(env) ? Object.fromEntries(
9511
+ Object.entries(env).map(([k, v]) => [k, String(v)])
9512
+ ) : void 0
9513
+ };
9514
+ }
9515
+
9283
9516
  // src/runtime/workflow-props.ts
9284
9517
  function typeOf(value) {
9285
9518
  if (value === null) return "null";
@@ -9358,6 +9591,25 @@ async function runFlow(graph, executors, onEvent = () => {
9358
9591
  const props = propsCheck.props;
9359
9592
  const declaresProps = (graph.inputs?.length ?? 0) > 0;
9360
9593
  const incomingByNode = indexIncoming(graph.edges);
9594
+ const sessions = new TerminalSessions(graph);
9595
+ const isTerminalLane = (candidate) => getNodeKind(candidate.type ?? "")?.name === "@particle-academy/terminal_lane";
9596
+ const terminalAccessorFor = (node) => {
9597
+ const laneId = sessions.laneFor(node.id, isTerminalLane);
9598
+ if (laneId === null) return void 0;
9599
+ const lane = graph.nodes.find((n) => n.id === laneId);
9600
+ if (!lane) return void 0;
9601
+ return {
9602
+ session: () => sessions.session(laneId, specForLane(lane)),
9603
+ // Awaits the session first on purpose. A transcript handed out before
9604
+ // anything is feeding it would look perfectly healthy and match nothing,
9605
+ // and "waited and nothing came" is the hardest failure here to tell apart
9606
+ // from a process that is simply slow.
9607
+ transcript: async () => {
9608
+ await sessions.session(laneId, specForLane(lane));
9609
+ return sessions.transcriptFor(laneId);
9610
+ }
9611
+ };
9612
+ };
9361
9613
  const timer2 = timeoutMs ? setTimeout(() => errors.push(`Run timed out after ${timeoutMs}ms`), timeoutMs) : null;
9362
9614
  onEvent({ type: "run-start" });
9363
9615
  try {
@@ -9423,7 +9675,8 @@ async function runFlow(graph, executors, onEvent = () => {
9423
9675
  emit: onEvent,
9424
9676
  executors,
9425
9677
  depth,
9426
- run
9678
+ run,
9679
+ terminal: terminalAccessorFor(node)
9427
9680
  })
9428
9681
  );
9429
9682
  outputs[node.id] = result;
@@ -9445,6 +9698,9 @@ async function runFlow(graph, executors, onEvent = () => {
9445
9698
  }
9446
9699
  } finally {
9447
9700
  if (timer2) clearTimeout(timer2);
9701
+ for (const error of await sessions.closeAll()) {
9702
+ onEvent({ type: "log", level: "warn", message: `terminal close failed: ${error.message}` });
9703
+ }
9448
9704
  }
9449
9705
  const ok = errors.length === 0;
9450
9706
  onEvent({ type: "run-end", ok });
@@ -9523,7 +9779,7 @@ function pickExecutor(executors, node) {
9523
9779
  for (const id2 of executorLookupIds(node)) {
9524
9780
  if (executors[id2]) return executors[id2];
9525
9781
  }
9526
- return void 0;
9782
+ return getNodeKind(node.type ?? "")?.executor;
9527
9783
  }
9528
9784
  function activatedPorts(node, result) {
9529
9785
  if (result && typeof result === "object") {
@@ -9633,6 +9889,366 @@ var subflowExecutor = async (ctx) => {
9633
9889
  return { __port: "out", value: result.outputs };
9634
9890
  };
9635
9891
 
9892
+ // src/expressions/expr.ts
9893
+ function wholeExpression(trimmed) {
9894
+ if (trimmed.length < 4) return null;
9895
+ if (!trimmed.startsWith("{{") || !trimmed.endsWith("}}")) return null;
9896
+ return trimmed.slice(2, -2);
9897
+ }
9898
+ function interpolate(template, resolve2) {
9899
+ let out = "";
9900
+ let i = 0;
9901
+ for (; ; ) {
9902
+ const open = template.indexOf("{{", i);
9903
+ if (open === -1) return out + template.slice(i);
9904
+ const close = template.indexOf("}}", open + 2);
9905
+ if (close === -1) return out + template.slice(i);
9906
+ out += template.slice(i, open) + resolve2(template.slice(open + 2, close));
9907
+ i = close + 2;
9908
+ }
9909
+ }
9910
+ var FALSY_STRINGS = /* @__PURE__ */ new Set(["", "0", "false", "no", "off", "null"]);
9911
+ var UnresolvedPathError = class extends Error {
9912
+ constructor(path) {
9913
+ super(
9914
+ `Expression path "${path}" did not resolve. Under the "throw" policy an unresolvable path is an error rather than an empty string.`
9915
+ );
9916
+ this.path = path;
9917
+ this.name = "UnresolvedPathError";
9918
+ }
9919
+ };
9920
+ function tryResolvePath(path, context) {
9921
+ const unresolved = { resolved: false, value: null };
9922
+ const trimmed = path.trim();
9923
+ if (trimmed === "") return unresolved;
9924
+ const segments = trimmed.split(".");
9925
+ let cursor;
9926
+ const head = segments[0];
9927
+ if (head === "$json" || head === "$input") {
9928
+ cursor = context !== null && typeof context === "object" && "in" in context ? context.in : context;
9929
+ segments.shift();
9930
+ } else {
9931
+ cursor = context;
9932
+ }
9933
+ for (const segment of segments) {
9934
+ if (cursor === null || cursor === void 0) return unresolved;
9935
+ if (typeof cursor !== "object") return unresolved;
9936
+ const next = cursor[segment];
9937
+ if (next === void 0) return unresolved;
9938
+ cursor = next;
9939
+ }
9940
+ return { resolved: true, value: cursor === void 0 ? null : cursor };
9941
+ }
9942
+ function evaluateExpression(template, context, options = {}) {
9943
+ if (typeof template !== "string") return template;
9944
+ const policy = options.onUnresolved ?? "empty";
9945
+ const whole = wholeExpression(template.trim());
9946
+ if (whole !== null) {
9947
+ const r = tryResolvePath(whole, context);
9948
+ if (r.resolved) return r.value;
9949
+ if (policy === "throw") throw new UnresolvedPathError(whole);
9950
+ return policy === "keep" ? template : null;
9951
+ }
9952
+ return interpolate(template, (path) => {
9953
+ const r = tryResolvePath(path, context);
9954
+ if (r.resolved) return stringify(r.value);
9955
+ if (policy === "throw") throw new UnresolvedPathError(path);
9956
+ return policy === "keep" ? `{{${path}}}` : "";
9957
+ });
9958
+ }
9959
+ function truthy(value) {
9960
+ if (typeof value === "boolean") return value;
9961
+ if (value === null || value === void 0) return false;
9962
+ if (typeof value === "string") return !FALSY_STRINGS.has(value.trim().toLowerCase());
9963
+ if (Array.isArray(value)) return value.length > 0;
9964
+ if (typeof value === "number") return value !== 0;
9965
+ return Boolean(value);
9966
+ }
9967
+ function text(value) {
9968
+ return stringify(value);
9969
+ }
9970
+ function stringify(value) {
9971
+ if (typeof value === "string") return value;
9972
+ if (typeof value === "boolean") return value ? "true" : "false";
9973
+ if (value === null || value === void 0) return "";
9974
+ if (typeof value === "number" || typeof value === "bigint") return String(value);
9975
+ try {
9976
+ return JSON.stringify(value) ?? "";
9977
+ } catch {
9978
+ return "";
9979
+ }
9980
+ }
9981
+
9982
+ // src/registry/logic.ts
9983
+ function configOf(node) {
9984
+ return node?.data?.config ?? {};
9985
+ }
9986
+ function contextOf(inputs) {
9987
+ return inputs;
9988
+ }
9989
+ function resolve(value, inputs) {
9990
+ if (typeof value !== "string") return value;
9991
+ return evaluateExpression(value, contextOf(inputs));
9992
+ }
9993
+ function conditionHolds(row, inputs) {
9994
+ const left = resolve(row.left, inputs);
9995
+ const right = resolve(row.right, inputs);
9996
+ const operator = typeof row.operator === "string" ? row.operator : "eq";
9997
+ const asText = (v) => text(v);
9998
+ const asNumber = (v) => {
9999
+ const n = typeof v === "number" ? v : Number(asText(v));
10000
+ return Number.isFinite(n) ? n : NaN;
10001
+ };
10002
+ const numeric = (fn) => {
10003
+ const a = asNumber(left);
10004
+ const b = asNumber(right);
10005
+ return Number.isFinite(a) && Number.isFinite(b) ? fn(a, b) : false;
10006
+ };
10007
+ 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;
10008
+ switch (operator) {
10009
+ // Compared as TEXT, deliberately. `right` is a text field in the schema, so
10010
+ // `status == "2"` must match the number 2 an upstream node produced —
10011
+ // strict equality there would fail on a difference the author cannot see.
10012
+ case "eq":
10013
+ return asText(left) === asText(right);
10014
+ case "neq":
10015
+ return asText(left) !== asText(right);
10016
+ case "contains":
10017
+ return asText(left).includes(asText(right));
10018
+ case "not_contains":
10019
+ return !asText(left).includes(asText(right));
10020
+ case "gt":
10021
+ return numeric((a, b) => a > b);
10022
+ case "gte":
10023
+ return numeric((a, b) => a >= b);
10024
+ case "lt":
10025
+ return numeric((a, b) => a < b);
10026
+ case "lte":
10027
+ return numeric((a, b) => a <= b);
10028
+ case "truthy":
10029
+ return truthy(left);
10030
+ case "falsy":
10031
+ return !truthy(left);
10032
+ case "empty":
10033
+ return isEmpty(left);
10034
+ case "not_empty":
10035
+ return !isEmpty(left);
10036
+ default:
10037
+ return false;
10038
+ }
10039
+ }
10040
+ var branchExecutor = (ctx) => {
10041
+ const config = configOf(ctx.node);
10042
+ const inputs = ctx.inputs;
10043
+ let taken;
10044
+ const raw = config.condition;
10045
+ if (typeof raw === "string" && raw.trim() !== "") {
10046
+ taken = truthy(resolve(raw, inputs));
10047
+ } else {
10048
+ const rows = Array.isArray(config.conditions) ? config.conditions : [];
10049
+ if (rows.length === 0) {
10050
+ taken = false;
10051
+ } else {
10052
+ const results = rows.map((row) => conditionHolds(row, inputs));
10053
+ taken = config.match === "any" ? results.some(Boolean) : results.every(Boolean);
10054
+ }
10055
+ }
10056
+ return { __port: taken ? "true" : "false", value: ctx.inputs.in ?? ctx.inputs };
10057
+ };
10058
+ var transformExecutor = (ctx) => {
10059
+ const config = configOf(ctx.node);
10060
+ const inputs = ctx.inputs;
10061
+ const passthrough = ctx.inputs.in ?? ctx.inputs;
10062
+ if (config.mode === "expression") {
10063
+ const expression = config.expression;
10064
+ if (typeof expression !== "string" || expression.trim() === "") return passthrough;
10065
+ return resolve(expression, inputs);
10066
+ }
10067
+ const rows = Array.isArray(config.fields) ? config.fields : [];
10068
+ if (rows.length === 0) return passthrough;
10069
+ const out = {};
10070
+ let wrote = false;
10071
+ for (const row of rows) {
10072
+ const key = typeof row.key === "string" ? row.key.trim() : "";
10073
+ if (key === "") continue;
10074
+ out[key] = resolve(row.value, inputs);
10075
+ wrote = true;
10076
+ }
10077
+ return wrote ? out : passthrough;
10078
+ };
10079
+ var mergeExecutor = (ctx) => {
10080
+ const entries = Object.entries(ctx.inputs);
10081
+ if (configOf(ctx.node).mode === "concat") {
10082
+ const out = [];
10083
+ for (const [, value] of entries) {
10084
+ if (value === null || value === void 0) continue;
10085
+ if (Array.isArray(value)) out.push(...value);
10086
+ else out.push(value);
10087
+ }
10088
+ return out;
10089
+ }
10090
+ const merged = {};
10091
+ for (const [port, value] of entries) {
10092
+ if (value === null || value === void 0) continue;
10093
+ if (typeof value === "object" && !Array.isArray(value)) Object.assign(merged, value);
10094
+ else merged[port] = value;
10095
+ }
10096
+ return merged;
10097
+ };
10098
+ var forEachExecutor = (ctx) => {
10099
+ const source = resolve(configOf(ctx.node).source, ctx.inputs);
10100
+ let items;
10101
+ if (Array.isArray(source)) items = source;
10102
+ else if (source === null || source === void 0) items = [];
10103
+ else if (typeof source === "object") items = Object.values(source);
10104
+ else items = [source];
10105
+ return { items, count: items.length };
10106
+ };
10107
+
10108
+ // src/registry/terminal-nodes.ts
10109
+ var DEFAULT_TIMEOUT_MS = 12e4;
10110
+ var ENTER = "\r";
10111
+ function configOf2(node) {
10112
+ return node?.data?.config ?? {};
10113
+ }
10114
+ function text2(config, key) {
10115
+ const value = config[key];
10116
+ return typeof value === "string" ? value : "";
10117
+ }
10118
+ function millis(config, key, fallback) {
10119
+ const value = Number(config[key]);
10120
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
10121
+ }
10122
+ function exitMarker(nonce) {
10123
+ const token = `__fancy_flow_exit_${nonce}__`;
10124
+ return {
10125
+ token,
10126
+ // The expanded status is required to be digits. The shell ECHOES the typed
10127
+ // command back first, and that echo contains the marker with `$?`
10128
+ // unexpanded — so requiring digits is what stops the node matching its own
10129
+ // command line and reporting success before anything has run.
10130
+ pattern: new RegExp(`${token}:(\\d+)`)
10131
+ };
10132
+ }
10133
+ var counter = 0;
10134
+ function newNonce() {
10135
+ counter += 1;
10136
+ return `${Date.now().toString(36)}${counter.toString(36)}${Math.random().toString(36).slice(2, 8)}`;
10137
+ }
10138
+ function withoutMarker(output, token) {
10139
+ return output.split("\n").filter((line) => !line.includes(token)).join("\n").trim();
10140
+ }
10141
+ function fail(ctx, message) {
10142
+ ctx.abort(message);
10143
+ throw new Error(message);
10144
+ }
10145
+ function requireTerminal(ctx) {
10146
+ if (!ctx.terminal) {
10147
+ fail(
10148
+ ctx,
10149
+ `"${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.`
10150
+ );
10151
+ }
10152
+ return ctx.terminal;
10153
+ }
10154
+ function compilePattern(ctx, raw, config) {
10155
+ const flags = typeof config.flags === "string" ? config.flags.replace(/[gy]/g, "") : "";
10156
+ try {
10157
+ return config.mode === "regex" ? new RegExp(raw, flags) : new RegExp(raw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), flags);
10158
+ } catch (e) {
10159
+ return fail(ctx, `terminal_await has an invalid regex: ${e instanceof Error ? e.message : String(e)}`);
10160
+ }
10161
+ }
10162
+ var terminalRunExecutor = async (ctx) => {
10163
+ const config = configOf2(ctx.node);
10164
+ const command = text2(config, "command").trim();
10165
+ if (!command) fail(ctx, "terminal_run has no command configured");
10166
+ const terminal = requireTerminal(ctx);
10167
+ const session = await terminal.session();
10168
+ const transcript = await terminal.transcript();
10169
+ const { token, pattern } = exitMarker(newNonce());
10170
+ const timeoutMs = millis(config, "timeoutMs", DEFAULT_TIMEOUT_MS);
10171
+ transcript.clear();
10172
+ await session.write(`${command}; printf '${token}:%s\\n' "$?"${ENTER}`);
10173
+ const result = await transcript.waitFor(pattern, { timeoutMs, exited: session.exited });
10174
+ if (result.status === "exited") {
10175
+ fail(
10176
+ ctx,
10177
+ `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.`
10178
+ );
10179
+ }
10180
+ if (result.status === "timeout") {
10181
+ fail(
10182
+ ctx,
10183
+ `"${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.`
10184
+ );
10185
+ }
10186
+ const exitCode = Number(result.match[1]);
10187
+ const output = withoutMarker(result.text, token);
10188
+ const failOnNonZero = config.failOnNonZero !== false;
10189
+ if (failOnNonZero && exitCode !== 0) {
10190
+ ctx.abort(`"${command}" exited ${exitCode}.
10191
+ ${output}`);
10192
+ }
10193
+ return { output, exitCode, command };
10194
+ };
10195
+ var terminalSendExecutor = async (ctx) => {
10196
+ const config = configOf2(ctx.node);
10197
+ const terminal = requireTerminal(ctx);
10198
+ const session = await terminal.session();
10199
+ const body = text2(config, "text");
10200
+ const submit = config.submit !== false;
10201
+ if (config.clearFirst === true) {
10202
+ (await terminal.transcript()).clear();
10203
+ }
10204
+ await session.write(submit ? `${body}${ENTER}` : body);
10205
+ return { sent: body, submitted: submit };
10206
+ };
10207
+ var terminalAwaitExecutor = async (ctx) => {
10208
+ const config = configOf2(ctx.node);
10209
+ const terminal = requireTerminal(ctx);
10210
+ const session = await terminal.session();
10211
+ const transcript = await terminal.transcript();
10212
+ const raw = text2(config, "pattern").trim();
10213
+ if (!raw) fail(ctx, "terminal_await has no pattern configured \u2014 there is nothing for it to wait for");
10214
+ const pattern = compilePattern(ctx, raw, config);
10215
+ const timeoutMs = millis(config, "timeoutMs", DEFAULT_TIMEOUT_MS);
10216
+ const result = await transcript.waitFor(pattern, {
10217
+ timeoutMs,
10218
+ exited: session.exited
10219
+ });
10220
+ if (result.status === "exited") {
10221
+ fail(
10222
+ ctx,
10223
+ `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.`
10224
+ );
10225
+ }
10226
+ if (result.status === "timeout") {
10227
+ if (config.onTimeout === "continue") {
10228
+ ctx.emit({
10229
+ type: "log",
10230
+ nodeId: ctx.node.id,
10231
+ level: "warn",
10232
+ message: `terminal_await: ${JSON.stringify(raw)} did not appear within ${timeoutMs}ms; continuing as configured.`
10233
+ });
10234
+ return { matched: false, output: result.text, groups: [] };
10235
+ }
10236
+ fail(
10237
+ ctx,
10238
+ `${JSON.stringify(raw)} did not appear within ${timeoutMs}ms. Last output:
10239
+ ${result.text.slice(-2e3)}`
10240
+ );
10241
+ }
10242
+ return {
10243
+ matched: true,
10244
+ output: result.text,
10245
+ matchedText: result.match[0],
10246
+ // Capture groups are the reason to use regex mode at all — a prompt that
10247
+ // reports a session id or a file path is only useful if the value comes out.
10248
+ groups: result.match.slice(1)
10249
+ };
10250
+ };
10251
+
9636
10252
  // src/registry/builtin.ts
9637
10253
  var HUMAN_FIELD_OUTPUT_TYPE = {
9638
10254
  text: "string",
@@ -9922,6 +10538,9 @@ var KINDS = [
9922
10538
  // returns its input on the chosen port
9923
10539
  emits: "input",
9924
10540
  aliases: ["branch", "@fancy/branch"],
10541
+ // Pure logic: no I/O to choose, so a default is safe. A host
10542
+ // executor still wins -- pickExecutor consults the registry first.
10543
+ executor: branchExecutor,
9925
10544
  category: "logic",
9926
10545
  label: "Branch",
9927
10546
  description: "Multi-way branch on a condition or value.",
@@ -10019,6 +10638,9 @@ var KINDS = [
10019
10638
  { path: "count", type: "number" }
10020
10639
  ],
10021
10640
  aliases: ["for_each", "@fancy/for_each"],
10641
+ // Pure logic: no I/O to choose, so a default is safe. A host
10642
+ // executor still wins -- pickExecutor consults the registry first.
10643
+ executor: forEachExecutor,
10022
10644
  category: "logic",
10023
10645
  label: "For Each",
10024
10646
  description: "Iterate over a list, emitting each item on `item`.",
@@ -10039,6 +10661,9 @@ var KINDS = [
10039
10661
  // reference.
10040
10662
  emits: (config) => (config?.mode ?? "merge") === "concat" ? null : "inputs-merged",
10041
10663
  aliases: ["merge", "@fancy/merge"],
10664
+ // Pure logic: no I/O to choose, so a default is safe. A host
10665
+ // executor still wins -- pickExecutor consults the registry first.
10666
+ executor: mergeExecutor,
10042
10667
  category: "logic",
10043
10668
  label: "Merge",
10044
10669
  description: "Combine multiple inputs into one object or array.",
@@ -10084,6 +10709,9 @@ var KINDS = [
10084
10709
  // else the shape that expression names. So the RELATION is config-dependent.
10085
10710
  emits: (config) => (config?.expression ?? "") === "" ? "input" : "expression:expression",
10086
10711
  aliases: ["transform", "@fancy/transform"],
10712
+ // Pure logic: no I/O to choose, so a default is safe. A host
10713
+ // executor still wins -- pickExecutor consults the registry first.
10714
+ executor: transformExecutor,
10087
10715
  category: "logic",
10088
10716
  label: "Transform",
10089
10717
  description: "Reshape data with an expression.",
@@ -10542,6 +11170,179 @@ var KINDS = [
10542
11170
  resizable: { minWidth: 160, minHeight: 72 },
10543
11171
  component: LaneNode
10544
11172
  },
11173
+ {
11174
+ // Terminal lane — a lane that OWNS a terminal for the length of the run.
11175
+ //
11176
+ // Visually a swimlane, and `layout` for the same reason the plain lane is:
11177
+ // a lane is not a step, so it never executes and never appears in topo
11178
+ // order. What is new is that it DECLARES a resource. The runtime opens its
11179
+ // terminal lazily, when the first terminal node inside it runs, and closes
11180
+ // it in the run's `finally` — so a graph that never reaches a terminal node
11181
+ // never spawns a process, and one that does gets exactly one session no
11182
+ // matter how many nodes use it.
11183
+ //
11184
+ // Membership is `parentId`, which already persists into the WorkflowSchema.
11185
+ // That is what lets a headless runtime resolve the same grouping the canvas
11186
+ // shows, without a second association to keep in sync.
11187
+ name: "@particle-academy/terminal_lane",
11188
+ aliases: ["terminal_lane", "@fancy/terminal_lane"],
11189
+ category: "layout",
11190
+ label: "Terminal lane",
11191
+ description: "A lane that owns one terminal. Opens at the first terminal node, closes when the run ends.",
11192
+ icon: "\u25A3",
11193
+ inputs: [],
11194
+ outputs: [],
11195
+ configSchema: [
11196
+ { type: "text", key: "title", label: "Title", default: "Terminal" },
11197
+ {
11198
+ type: "text",
11199
+ key: "command",
11200
+ label: "Command",
11201
+ placeholder: "Leave empty for the default shell"
11202
+ },
11203
+ { type: "text", key: "cwd", label: "Working directory", placeholder: "Host default" },
11204
+ { type: "keyvalue", key: "env", label: "Environment" },
11205
+ {
11206
+ type: "select",
11207
+ key: "orientation",
11208
+ label: "Orientation",
11209
+ default: "horizontal",
11210
+ options: [
11211
+ { value: "horizontal", label: "Row" },
11212
+ { value: "vertical", label: "Column" }
11213
+ ]
11214
+ }
11215
+ ],
11216
+ defaultConfig: { title: "Terminal", orientation: "horizontal" },
11217
+ resizable: { minWidth: 220, minHeight: 120 },
11218
+ component: LaneNode
11219
+ },
11220
+ {
11221
+ // Terminal nodes. All three live inside a terminal lane and talk to the
11222
+ // session that lane owns; outside one they abort by name rather than
11223
+ // opening a shell of their own.
11224
+ name: "@particle-academy/terminal_run",
11225
+ aliases: ["terminal_run", "@fancy/terminal_run"],
11226
+ category: "io",
11227
+ label: "Run in terminal",
11228
+ description: "Run a shell command in the lane's terminal and wait for its exit code.",
11229
+ icon: "$",
11230
+ inputs: [{ id: "in" }],
11231
+ outputs: [{ id: "out", label: "output" }],
11232
+ executor: terminalRunExecutor,
11233
+ configSchema: [
11234
+ {
11235
+ type: "textarea",
11236
+ key: "command",
11237
+ label: "Command",
11238
+ rows: 3,
11239
+ required: true,
11240
+ placeholder: "npm test",
11241
+ 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."
11242
+ },
11243
+ {
11244
+ type: "number",
11245
+ key: "timeoutMs",
11246
+ label: "Timeout (ms)",
11247
+ default: 12e4,
11248
+ description: "How long to wait for the command to finish before failing the run."
11249
+ },
11250
+ {
11251
+ type: "switch",
11252
+ key: "failOnNonZero",
11253
+ label: "Fail the run on a non-zero exit",
11254
+ default: true,
11255
+ 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."
11256
+ }
11257
+ ],
11258
+ defaultConfig: { command: "", timeoutMs: 12e4, failOnNonZero: true }
11259
+ },
11260
+ {
11261
+ name: "@particle-academy/terminal_send",
11262
+ aliases: ["terminal_send", "@fancy/terminal_send"],
11263
+ category: "io",
11264
+ label: "Send to terminal",
11265
+ description: "Type text at whatever is running in the lane's terminal, without waiting.",
11266
+ icon: "\u2328",
11267
+ inputs: [{ id: "in" }],
11268
+ outputs: [{ id: "out", label: "sent" }],
11269
+ executor: terminalSendExecutor,
11270
+ configSchema: [
11271
+ {
11272
+ type: "textarea",
11273
+ key: "text",
11274
+ label: "Text",
11275
+ rows: 4,
11276
+ placeholder: "Summarise the failing test and propose a fix.",
11277
+ description: "What to type. This is how a graph prompts an agent TUI such as Claude Code or Codex."
11278
+ },
11279
+ {
11280
+ type: "switch",
11281
+ key: "submit",
11282
+ label: "Press Enter",
11283
+ default: true,
11284
+ description: "Off leaves the text on the input line \u2014 useful for building one up across several nodes."
11285
+ },
11286
+ {
11287
+ type: "switch",
11288
+ key: "clearFirst",
11289
+ label: "Forget earlier output first",
11290
+ default: false,
11291
+ description: "Discards anything the terminal said before this send, so a following Await cannot match a prompt left over from the previous exchange."
11292
+ }
11293
+ ],
11294
+ defaultConfig: { text: "", submit: true, clearFirst: false }
11295
+ },
11296
+ {
11297
+ name: "@particle-academy/terminal_await",
11298
+ aliases: ["terminal_await", "@fancy/terminal_await"],
11299
+ category: "io",
11300
+ label: "Await terminal output",
11301
+ description: "Wait until the lane's terminal prints something that matches.",
11302
+ icon: "\u23F1",
11303
+ inputs: [{ id: "in" }],
11304
+ outputs: [{ id: "out", label: "output" }],
11305
+ executor: terminalAwaitExecutor,
11306
+ configSchema: [
11307
+ {
11308
+ type: "text",
11309
+ key: "pattern",
11310
+ label: "Wait for",
11311
+ required: true,
11312
+ placeholder: "esc to interrupt",
11313
+ description: "Matched against the output with colour codes already stripped, so match what you SEE."
11314
+ },
11315
+ {
11316
+ type: "select",
11317
+ key: "mode",
11318
+ label: "Match as",
11319
+ default: "text",
11320
+ options: [
11321
+ { value: "text", label: "Plain text" },
11322
+ { value: "regex", label: "Regular expression" }
11323
+ ],
11324
+ description: "Regex mode also returns capture groups, which is how a value gets out of a prompt."
11325
+ },
11326
+ {
11327
+ type: "number",
11328
+ key: "timeoutMs",
11329
+ label: "Timeout (ms)",
11330
+ default: 12e4
11331
+ },
11332
+ {
11333
+ type: "select",
11334
+ key: "onTimeout",
11335
+ label: "If it never appears",
11336
+ default: "fail",
11337
+ options: [
11338
+ { value: "fail", label: "Fail the run" },
11339
+ { value: "continue", label: "Continue with matched: false" }
11340
+ ],
11341
+ 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."
11342
+ }
11343
+ ],
11344
+ defaultConfig: { pattern: "", mode: "text", timeoutMs: 12e4, onTimeout: "fail" }
11345
+ },
10545
11346
  {
10546
11347
  // Note — a sticky-note annotation. Portless + visual-only: the runtime skips
10547
11348
  // the `annotation` category, so a note's text NEVER reaches a runner — it