@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/screens.cjs CHANGED
@@ -2094,38 +2094,38 @@ function attrRemoveNS2(fullname) {
2094
2094
  this.removeAttributeNS(fullname.space, fullname.local);
2095
2095
  };
2096
2096
  }
2097
- function attrConstant2(name, interpolate, value1) {
2097
+ function attrConstant2(name, interpolate2, value1) {
2098
2098
  var string00, string1 = value1 + "", interpolate0;
2099
2099
  return function() {
2100
2100
  var string0 = this.getAttribute(name);
2101
- return string0 === string1 ? null : string0 === string00 ? interpolate0 : interpolate0 = interpolate(string00 = string0, value1);
2101
+ return string0 === string1 ? null : string0 === string00 ? interpolate0 : interpolate0 = interpolate2(string00 = string0, value1);
2102
2102
  };
2103
2103
  }
2104
- function attrConstantNS2(fullname, interpolate, value1) {
2104
+ function attrConstantNS2(fullname, interpolate2, value1) {
2105
2105
  var string00, string1 = value1 + "", interpolate0;
2106
2106
  return function() {
2107
2107
  var string0 = this.getAttributeNS(fullname.space, fullname.local);
2108
- return string0 === string1 ? null : string0 === string00 ? interpolate0 : interpolate0 = interpolate(string00 = string0, value1);
2108
+ return string0 === string1 ? null : string0 === string00 ? interpolate0 : interpolate0 = interpolate2(string00 = string0, value1);
2109
2109
  };
2110
2110
  }
2111
- function attrFunction2(name, interpolate, value) {
2111
+ function attrFunction2(name, interpolate2, value) {
2112
2112
  var string00, string10, interpolate0;
2113
2113
  return function() {
2114
2114
  var string0, value1 = value(this), string1;
2115
2115
  if (value1 == null) return void this.removeAttribute(name);
2116
2116
  string0 = this.getAttribute(name);
2117
2117
  string1 = value1 + "";
2118
- return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));
2118
+ return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : (string10 = string1, interpolate0 = interpolate2(string00 = string0, value1));
2119
2119
  };
2120
2120
  }
2121
- function attrFunctionNS2(fullname, interpolate, value) {
2121
+ function attrFunctionNS2(fullname, interpolate2, value) {
2122
2122
  var string00, string10, interpolate0;
2123
2123
  return function() {
2124
2124
  var string0, value1 = value(this), string1;
2125
2125
  if (value1 == null) return void this.removeAttributeNS(fullname.space, fullname.local);
2126
2126
  string0 = this.getAttributeNS(fullname.space, fullname.local);
2127
2127
  string1 = value1 + "";
2128
- return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));
2128
+ return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : (string10 = string1, interpolate0 = interpolate2(string00 = string0, value1));
2129
2129
  };
2130
2130
  }
2131
2131
  function attr_default2(name, value) {
@@ -2335,11 +2335,11 @@ function selection_default2() {
2335
2335
  }
2336
2336
 
2337
2337
  // node_modules/d3-transition/src/transition/style.js
2338
- function styleNull(name, interpolate) {
2338
+ function styleNull(name, interpolate2) {
2339
2339
  var string00, string10, interpolate0;
2340
2340
  return function() {
2341
2341
  var string0 = styleValue(this, name), string1 = (this.style.removeProperty(name), styleValue(this, name));
2342
- return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : interpolate0 = interpolate(string00 = string0, string10 = string1);
2342
+ return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : interpolate0 = interpolate2(string00 = string0, string10 = string1);
2343
2343
  };
2344
2344
  }
2345
2345
  function styleRemove2(name) {
@@ -2347,19 +2347,19 @@ function styleRemove2(name) {
2347
2347
  this.style.removeProperty(name);
2348
2348
  };
2349
2349
  }
2350
- function styleConstant2(name, interpolate, value1) {
2350
+ function styleConstant2(name, interpolate2, value1) {
2351
2351
  var string00, string1 = value1 + "", interpolate0;
2352
2352
  return function() {
2353
2353
  var string0 = styleValue(this, name);
2354
- return string0 === string1 ? null : string0 === string00 ? interpolate0 : interpolate0 = interpolate(string00 = string0, value1);
2354
+ return string0 === string1 ? null : string0 === string00 ? interpolate0 : interpolate0 = interpolate2(string00 = string0, value1);
2355
2355
  };
2356
2356
  }
2357
- function styleFunction2(name, interpolate, value) {
2357
+ function styleFunction2(name, interpolate2, value) {
2358
2358
  var string00, string10, interpolate0;
2359
2359
  return function() {
2360
2360
  var string0 = styleValue(this, name), value1 = value(this), string1 = value1 + "";
2361
2361
  if (value1 == null) string1 = value1 = (this.style.removeProperty(name), styleValue(this, name));
2362
- return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));
2362
+ return string0 === string1 ? null : string0 === string00 && string1 === string10 ? interpolate0 : (string10 = string1, interpolate0 = interpolate2(string00 = string0, value1));
2363
2363
  };
2364
2364
  }
2365
2365
  function styleMaybeRemove(id2, name) {
@@ -2461,9 +2461,9 @@ function transition_default() {
2461
2461
  // node_modules/d3-transition/src/transition/end.js
2462
2462
  function end_default() {
2463
2463
  var on0, on1, that = this, id2 = that._id, size = that.size();
2464
- return new Promise(function(resolve, reject) {
2464
+ return new Promise(function(resolve2, reject) {
2465
2465
  var cancel = { value: reject }, end = { value: function() {
2466
- if (--size === 0) resolve();
2466
+ if (--size === 0) resolve2();
2467
2467
  } };
2468
2468
  that.each(function() {
2469
2469
  var schedule = set2(this, id2), on = schedule.on;
@@ -2475,7 +2475,7 @@ function end_default() {
2475
2475
  }
2476
2476
  schedule.on = on1;
2477
2477
  });
2478
- if (size === 0) resolve();
2478
+ if (size === 0) resolve2();
2479
2479
  });
2480
2480
  }
2481
2481
 
@@ -2677,7 +2677,7 @@ function defaultConstrain(transform2, extent, translateExtent) {
2677
2677
  );
2678
2678
  }
2679
2679
  function zoom_default2() {
2680
- 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;
2680
+ 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;
2681
2681
  function zoom(selection2) {
2682
2682
  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)");
2683
2683
  }
@@ -2738,7 +2738,7 @@ function zoom_default2() {
2738
2738
  }).on("interrupt.zoom end.zoom", function() {
2739
2739
  gesture(this, arguments).event(event).end();
2740
2740
  }).tween("zoom", function() {
2741
- 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, w2 = 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(w2 / a.k), b.invert(p).concat(w2 / b.k));
2741
+ 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, w2 = 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(w2 / a.k), b.invert(p).concat(w2 / b.k));
2742
2742
  return function(t) {
2743
2743
  if (t === 1) t = b;
2744
2744
  else {
@@ -2943,7 +2943,7 @@ function zoom_default2() {
2943
2943
  return arguments.length ? (duration = +_, zoom) : duration;
2944
2944
  };
2945
2945
  zoom.interpolate = function(_) {
2946
- return arguments.length ? (interpolate = _, zoom) : interpolate;
2946
+ return arguments.length ? (interpolate2 = _, zoom) : interpolate2;
2947
2947
  };
2948
2948
  zoom.on = function() {
2949
2949
  var value = listeners2.on.apply(listeners2, arguments);
@@ -3436,13 +3436,13 @@ function areSetsEqual(a, b) {
3436
3436
  return true;
3437
3437
  }
3438
3438
  function withResolvers() {
3439
- let resolve;
3439
+ let resolve2;
3440
3440
  let reject;
3441
3441
  const promise = new Promise((res, rej) => {
3442
- resolve = res;
3442
+ resolve2 = res;
3443
3443
  reject = rej;
3444
3444
  });
3445
- return { promise, resolve, reject };
3445
+ return { promise, resolve: resolve2, reject };
3446
3446
  }
3447
3447
  function mergeAriaLabelConfig(partial) {
3448
3448
  return { ...defaultAriaLabelConfig, ...partial || {} };
@@ -5092,8 +5092,8 @@ function XYPanZoom({ domNode, minZoom, maxZoom, translateExtent, viewport, onPan
5092
5092
  d3ZoomInstance.wheelDelta(wheelDelta);
5093
5093
  async function setTransform(transform2, options) {
5094
5094
  if (d3Selection) {
5095
- return new Promise((resolve) => {
5096
- d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).transform(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)), transform2);
5095
+ return new Promise((resolve2) => {
5096
+ d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).transform(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve2(true)), transform2);
5097
5097
  });
5098
5098
  }
5099
5099
  return false;
@@ -5195,16 +5195,16 @@ function XYPanZoom({ domNode, minZoom, maxZoom, translateExtent, viewport, onPan
5195
5195
  }
5196
5196
  async function scaleTo(zoom, options) {
5197
5197
  if (d3Selection) {
5198
- return new Promise((resolve) => {
5199
- d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleTo(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)), zoom);
5198
+ return new Promise((resolve2) => {
5199
+ d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleTo(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve2(true)), zoom);
5200
5200
  });
5201
5201
  }
5202
5202
  return false;
5203
5203
  }
5204
5204
  async function scaleBy(factor, options) {
5205
5205
  if (d3Selection) {
5206
- return new Promise((resolve) => {
5207
- d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleBy(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)), factor);
5206
+ return new Promise((resolve2) => {
5207
+ d3ZoomInstance?.interpolate(options?.interpolate === "linear" ? value_default : zoom_default).scaleBy(getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve2(true)), factor);
5208
5208
  });
5209
5209
  }
5210
5210
  return false;
@@ -9129,11 +9129,11 @@ function NoteNodeInner(props) {
9129
9129
  const config = data.config ?? {};
9130
9130
  const api = useFlowEditorOptional();
9131
9131
  const hasConfigText = typeof config.text === "string" && config.text.length > 0;
9132
- const text = hasConfigText ? String(config.text) : typeof data.body === "string" ? data.body : typeof data.text === "string" ? data.text : "";
9132
+ const text3 = hasConfigText ? String(config.text) : typeof data.body === "string" ? data.body : typeof data.text === "string" ? data.text : "";
9133
9133
  const title = typeof config.title === "string" && config.title || (!hasConfigText && typeof data.body === "string" && typeof data.label === "string" ? data.label : "");
9134
9134
  const skin = NOTE_COLORS[coerceColor(config.color)];
9135
9135
  const [editing, setEditing] = ReactExports.useState(false);
9136
- const [draft, setDraft] = ReactExports.useState(text);
9136
+ const [draft, setDraft] = ReactExports.useState(text3);
9137
9137
  const areaRef = ReactExports.useRef(null);
9138
9138
  ReactExports.useEffect(() => {
9139
9139
  if (editing && areaRef.current) {
@@ -9144,7 +9144,7 @@ function NoteNodeInner(props) {
9144
9144
  }, [editing]);
9145
9145
  const commit = () => {
9146
9146
  setEditing(false);
9147
- if (!api || draft === text) return;
9147
+ if (!api || draft === text3) return;
9148
9148
  const current = api.nodes.find((n) => n.id === props.id);
9149
9149
  if (!current) return;
9150
9150
  api.updateNode({
@@ -9158,7 +9158,7 @@ function NoteNodeInner(props) {
9158
9158
  className: ["ff-note", props.selected ? "ff-note--selected" : ""].filter(Boolean).join(" "),
9159
9159
  style: { background: skin.bg, borderColor: skin.border },
9160
9160
  onDoubleClick: api ? () => {
9161
- setDraft(text);
9161
+ setDraft(text3);
9162
9162
  setEditing(true);
9163
9163
  } : void 0,
9164
9164
  children: [
@@ -9177,7 +9177,7 @@ function NoteNodeInner(props) {
9177
9177
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) commit();
9178
9178
  }
9179
9179
  }
9180
- ) : /* @__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" }) })
9180
+ ) : /* @__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" }) })
9181
9181
  ]
9182
9182
  }
9183
9183
  );
@@ -9464,6 +9464,239 @@ var RunIdentity = class _RunIdentity {
9464
9464
  }
9465
9465
  };
9466
9466
 
9467
+ // src/runtime/terminal-transcript.ts
9468
+ var ESC = String.fromCharCode(27);
9469
+ var CSI = String.fromCharCode(155);
9470
+ var BEL = String.fromCharCode(7);
9471
+ var ANSI = new RegExp(
9472
+ `[${ESC}${CSI}](?:\\[[0-?]*[ -/]*[@-~]|\\][^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)|[@-Z\\\\-_])`,
9473
+ "g"
9474
+ );
9475
+ var MAX_PENDING_ESCAPE = 1024;
9476
+ var MAX_BUFFER = 1e6;
9477
+ var TerminalTranscript = class {
9478
+ constructor() {
9479
+ /** Stripped, complete text not yet consumed by a wait. */
9480
+ this.text = "";
9481
+ /** Raw bytes held back because they may be the start of an escape sequence. */
9482
+ this.tail = "";
9483
+ this.waiters = /* @__PURE__ */ new Set();
9484
+ }
9485
+ /** Feed raw terminal output in. Safe to call with any chunking. */
9486
+ append(chunk) {
9487
+ this.tail += chunk;
9488
+ const safe = this.safeLength();
9489
+ if (safe > 0) {
9490
+ this.text += this.tail.slice(0, safe).replace(ANSI, "");
9491
+ this.tail = this.tail.slice(safe);
9492
+ }
9493
+ if (this.text.length > MAX_BUFFER) {
9494
+ this.text = this.text.slice(this.text.length - MAX_BUFFER);
9495
+ }
9496
+ for (const wake2 of [...this.waiters]) wake2();
9497
+ }
9498
+ /**
9499
+ * How much of `tail` can be stripped now.
9500
+ *
9501
+ * Everything, unless the final `ESC` has not yet been terminated — in which
9502
+ * case processing stops there and resumes when the rest arrives.
9503
+ */
9504
+ safeLength() {
9505
+ const lastEsc = Math.max(this.tail.lastIndexOf(ESC), this.tail.lastIndexOf(CSI));
9506
+ if (lastEsc === -1) return this.tail.length;
9507
+ ANSI.lastIndex = lastEsc;
9508
+ const match = ANSI.exec(this.tail);
9509
+ ANSI.lastIndex = 0;
9510
+ if (match && match.index === lastEsc) return this.tail.length;
9511
+ if (this.tail.length - lastEsc > MAX_PENDING_ESCAPE) return this.tail.length;
9512
+ return lastEsc;
9513
+ }
9514
+ /** Unconsumed output, escape sequences removed. */
9515
+ peek() {
9516
+ return this.text;
9517
+ }
9518
+ /** Drop everything currently buffered — used before typing a new command. */
9519
+ clear() {
9520
+ this.text = "";
9521
+ this.tail = "";
9522
+ }
9523
+ /**
9524
+ * Wait until `pattern` matches the unconsumed text.
9525
+ *
9526
+ * Checks what has ALREADY arrived before subscribing, because the common case
9527
+ * is that the process answered while the previous node was still finishing.
9528
+ *
9529
+ * `exited` is raced deliberately. Without it, a shell that dies reports as
9530
+ * "timed out waiting for X" — sending whoever reads it to lengthen a timeout
9531
+ * for a process that is not running. Naming the exit is the difference
9532
+ * between a diagnosis and a wrong lead.
9533
+ */
9534
+ waitFor(pattern, options) {
9535
+ return new Promise((resolve2) => {
9536
+ let settled = false;
9537
+ let timer2;
9538
+ const finish = (result) => {
9539
+ if (settled) return;
9540
+ settled = true;
9541
+ this.waiters.delete(check);
9542
+ if (timer2) clearTimeout(timer2);
9543
+ resolve2(result);
9544
+ };
9545
+ const check = () => {
9546
+ pattern.lastIndex = 0;
9547
+ const match = pattern.exec(this.text);
9548
+ if (!match) return;
9549
+ const through = match.index + (match[0].length || 1);
9550
+ const text3 = this.text.slice(0, through);
9551
+ this.text = this.text.slice(through);
9552
+ finish({ status: "matched", text: text3, match });
9553
+ };
9554
+ this.waiters.add(check);
9555
+ check();
9556
+ if (settled) return;
9557
+ if (options.timeoutMs > 0) {
9558
+ timer2 = setTimeout(() => {
9559
+ const text3 = this.text;
9560
+ this.text = "";
9561
+ finish({ status: "timeout", text: text3 });
9562
+ }, options.timeoutMs);
9563
+ timer2.unref?.();
9564
+ }
9565
+ options.exited?.then(
9566
+ (exit) => {
9567
+ if (settled) return;
9568
+ const text3 = this.text;
9569
+ this.text = "";
9570
+ finish({ status: "exited", text: text3, exitCode: exit.exitCode, signal: exit.signal });
9571
+ },
9572
+ () => {
9573
+ }
9574
+ );
9575
+ });
9576
+ }
9577
+ };
9578
+
9579
+ // src/runtime/terminal-sessions.ts
9580
+ var TerminalSessions = class {
9581
+ constructor(graph) {
9582
+ this.graph = graph;
9583
+ this.open = /* @__PURE__ */ new Map();
9584
+ this.laneOf = /* @__PURE__ */ new Map();
9585
+ this.transcripts = /* @__PURE__ */ new Map();
9586
+ this.listening = /* @__PURE__ */ new Map();
9587
+ }
9588
+ /**
9589
+ * The terminal lane a node belongs to, or null.
9590
+ *
9591
+ * Walks up `parentId` rather than checking only the immediate parent, and
9592
+ * caches per node id — a graph is walked once per run, and the answer cannot
9593
+ * change while it runs.
9594
+ */
9595
+ laneFor(nodeId, isTerminalLane) {
9596
+ const cached = this.laneOf.get(nodeId);
9597
+ if (cached !== void 0) return cached;
9598
+ const byId = new Map(this.graph.nodes.map((n) => [n.id, n]));
9599
+ const seen = /* @__PURE__ */ new Set();
9600
+ let current = byId.get(nodeId);
9601
+ let answer = null;
9602
+ while (current) {
9603
+ if (seen.has(current.id)) break;
9604
+ seen.add(current.id);
9605
+ if (current.id !== nodeId && isTerminalLane(current)) {
9606
+ answer = current.id;
9607
+ break;
9608
+ }
9609
+ const parentId = current.parentId;
9610
+ current = parentId ? byId.get(parentId) : void 0;
9611
+ }
9612
+ this.laneOf.set(nodeId, answer);
9613
+ return answer;
9614
+ }
9615
+ /**
9616
+ * The session for a lane, opening it on first use.
9617
+ *
9618
+ * The PROMISE is cached, not the resolved session. Two nodes cannot run
9619
+ * concurrently in the current engine, but a caller that awaits a
9620
+ * half-finished open would otherwise start a second process — and two shells
9621
+ * where a graph says one is the failure this class exists to prevent, arriving
9622
+ * only under concurrency and therefore only sometimes.
9623
+ */
9624
+ session(laneId, spec) {
9625
+ const existing = this.open.get(laneId);
9626
+ if (existing) return existing;
9627
+ {
9628
+ const failed = Promise.reject(
9629
+ new Error(
9630
+ "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."
9631
+ )
9632
+ );
9633
+ this.open.set(laneId, failed);
9634
+ failed.catch(() => {
9635
+ });
9636
+ return failed;
9637
+ }
9638
+ }
9639
+ /**
9640
+ * The accumulated output for a lane.
9641
+ *
9642
+ * Created on demand and kept for the run, so it exists before the session
9643
+ * resolves and survives every node that reads it. Consuming is the reader's
9644
+ * job — see `TerminalTranscript.waitFor`.
9645
+ */
9646
+ transcriptFor(laneId) {
9647
+ const existing = this.transcripts.get(laneId);
9648
+ if (existing) return existing;
9649
+ const created = new TerminalTranscript();
9650
+ this.transcripts.set(laneId, created);
9651
+ return created;
9652
+ }
9653
+ /** True once a lane has a session — used to avoid opening one during teardown. */
9654
+ isOpen(laneId) {
9655
+ return this.open.has(laneId);
9656
+ }
9657
+ /**
9658
+ * Close every session this run opened.
9659
+ *
9660
+ * Every close is attempted even if one throws: a host that fails to close one
9661
+ * PTY must not strand the others, which would leave processes alive after the
9662
+ * run reported that it had finished. Errors are returned rather than thrown,
9663
+ * because teardown runs in a `finally` and throwing there would replace the
9664
+ * run's real error with a cleanup error.
9665
+ */
9666
+ async closeAll() {
9667
+ const errors = [];
9668
+ for (const [laneId, pending] of this.open) {
9669
+ try {
9670
+ this.listening.get(laneId)?.();
9671
+ this.listening.delete(laneId);
9672
+ const session = await pending;
9673
+ await session.close();
9674
+ } catch (e) {
9675
+ errors.push(e instanceof Error ? e : new Error(`${laneId}: ${String(e)}`));
9676
+ }
9677
+ }
9678
+ this.open.clear();
9679
+ this.listening.clear();
9680
+ this.transcripts.clear();
9681
+ return errors;
9682
+ }
9683
+ };
9684
+ function specForLane(lane) {
9685
+ const config = lane.data?.config ?? {};
9686
+ const text3 = (key) => {
9687
+ const value = config[key];
9688
+ return typeof value === "string" && value !== "" ? value : void 0;
9689
+ };
9690
+ const env = config.env;
9691
+ return {
9692
+ command: text3("command"),
9693
+ cwd: text3("cwd"),
9694
+ env: env && typeof env === "object" && !Array.isArray(env) ? Object.fromEntries(
9695
+ Object.entries(env).map(([k, v]) => [k, String(v)])
9696
+ ) : void 0
9697
+ };
9698
+ }
9699
+
9467
9700
  // src/runtime/workflow-props.ts
9468
9701
  function typeOf(value) {
9469
9702
  if (value === null) return "null";
@@ -9542,6 +9775,25 @@ async function runFlow(graph, executors, onEvent = () => {
9542
9775
  const props = propsCheck.props;
9543
9776
  const declaresProps = (graph.inputs?.length ?? 0) > 0;
9544
9777
  const incomingByNode = indexIncoming(graph.edges);
9778
+ const sessions = new TerminalSessions(graph);
9779
+ const isTerminalLane = (candidate) => getNodeKind(candidate.type ?? "")?.name === "@particle-academy/terminal_lane";
9780
+ const terminalAccessorFor = (node) => {
9781
+ const laneId = sessions.laneFor(node.id, isTerminalLane);
9782
+ if (laneId === null) return void 0;
9783
+ const lane = graph.nodes.find((n) => n.id === laneId);
9784
+ if (!lane) return void 0;
9785
+ return {
9786
+ session: () => sessions.session(laneId, specForLane(lane)),
9787
+ // Awaits the session first on purpose. A transcript handed out before
9788
+ // anything is feeding it would look perfectly healthy and match nothing,
9789
+ // and "waited and nothing came" is the hardest failure here to tell apart
9790
+ // from a process that is simply slow.
9791
+ transcript: async () => {
9792
+ await sessions.session(laneId, specForLane(lane));
9793
+ return sessions.transcriptFor(laneId);
9794
+ }
9795
+ };
9796
+ };
9545
9797
  const timer2 = timeoutMs ? setTimeout(() => errors.push(`Run timed out after ${timeoutMs}ms`), timeoutMs) : null;
9546
9798
  onEvent({ type: "run-start" });
9547
9799
  try {
@@ -9607,7 +9859,8 @@ async function runFlow(graph, executors, onEvent = () => {
9607
9859
  emit: onEvent,
9608
9860
  executors,
9609
9861
  depth,
9610
- run
9862
+ run,
9863
+ terminal: terminalAccessorFor(node)
9611
9864
  })
9612
9865
  );
9613
9866
  outputs[node.id] = result;
@@ -9629,6 +9882,9 @@ async function runFlow(graph, executors, onEvent = () => {
9629
9882
  }
9630
9883
  } finally {
9631
9884
  if (timer2) clearTimeout(timer2);
9885
+ for (const error of await sessions.closeAll()) {
9886
+ onEvent({ type: "log", level: "warn", message: `terminal close failed: ${error.message}` });
9887
+ }
9632
9888
  }
9633
9889
  const ok = errors.length === 0;
9634
9890
  onEvent({ type: "run-end", ok });
@@ -9707,7 +9963,7 @@ function pickExecutor(executors, node) {
9707
9963
  for (const id2 of executorLookupIds(node)) {
9708
9964
  if (executors[id2]) return executors[id2];
9709
9965
  }
9710
- return void 0;
9966
+ return getNodeKind(node.type ?? "")?.executor;
9711
9967
  }
9712
9968
  function activatedPorts(node, result) {
9713
9969
  if (result && typeof result === "object") {
@@ -9817,6 +10073,366 @@ var subflowExecutor = async (ctx) => {
9817
10073
  return { __port: "out", value: result.outputs };
9818
10074
  };
9819
10075
 
10076
+ // src/expressions/expr.ts
10077
+ function wholeExpression(trimmed) {
10078
+ if (trimmed.length < 4) return null;
10079
+ if (!trimmed.startsWith("{{") || !trimmed.endsWith("}}")) return null;
10080
+ return trimmed.slice(2, -2);
10081
+ }
10082
+ function interpolate(template, resolve2) {
10083
+ let out = "";
10084
+ let i = 0;
10085
+ for (; ; ) {
10086
+ const open = template.indexOf("{{", i);
10087
+ if (open === -1) return out + template.slice(i);
10088
+ const close = template.indexOf("}}", open + 2);
10089
+ if (close === -1) return out + template.slice(i);
10090
+ out += template.slice(i, open) + resolve2(template.slice(open + 2, close));
10091
+ i = close + 2;
10092
+ }
10093
+ }
10094
+ var FALSY_STRINGS = /* @__PURE__ */ new Set(["", "0", "false", "no", "off", "null"]);
10095
+ var UnresolvedPathError = class extends Error {
10096
+ constructor(path) {
10097
+ super(
10098
+ `Expression path "${path}" did not resolve. Under the "throw" policy an unresolvable path is an error rather than an empty string.`
10099
+ );
10100
+ this.path = path;
10101
+ this.name = "UnresolvedPathError";
10102
+ }
10103
+ };
10104
+ function tryResolvePath(path, context) {
10105
+ const unresolved = { resolved: false, value: null };
10106
+ const trimmed = path.trim();
10107
+ if (trimmed === "") return unresolved;
10108
+ const segments = trimmed.split(".");
10109
+ let cursor;
10110
+ const head = segments[0];
10111
+ if (head === "$json" || head === "$input") {
10112
+ cursor = context !== null && typeof context === "object" && "in" in context ? context.in : context;
10113
+ segments.shift();
10114
+ } else {
10115
+ cursor = context;
10116
+ }
10117
+ for (const segment of segments) {
10118
+ if (cursor === null || cursor === void 0) return unresolved;
10119
+ if (typeof cursor !== "object") return unresolved;
10120
+ const next = cursor[segment];
10121
+ if (next === void 0) return unresolved;
10122
+ cursor = next;
10123
+ }
10124
+ return { resolved: true, value: cursor === void 0 ? null : cursor };
10125
+ }
10126
+ function evaluateExpression(template, context, options = {}) {
10127
+ if (typeof template !== "string") return template;
10128
+ const policy = options.onUnresolved ?? "empty";
10129
+ const whole = wholeExpression(template.trim());
10130
+ if (whole !== null) {
10131
+ const r = tryResolvePath(whole, context);
10132
+ if (r.resolved) return r.value;
10133
+ if (policy === "throw") throw new UnresolvedPathError(whole);
10134
+ return policy === "keep" ? template : null;
10135
+ }
10136
+ return interpolate(template, (path) => {
10137
+ const r = tryResolvePath(path, context);
10138
+ if (r.resolved) return stringify(r.value);
10139
+ if (policy === "throw") throw new UnresolvedPathError(path);
10140
+ return policy === "keep" ? `{{${path}}}` : "";
10141
+ });
10142
+ }
10143
+ function truthy(value) {
10144
+ if (typeof value === "boolean") return value;
10145
+ if (value === null || value === void 0) return false;
10146
+ if (typeof value === "string") return !FALSY_STRINGS.has(value.trim().toLowerCase());
10147
+ if (Array.isArray(value)) return value.length > 0;
10148
+ if (typeof value === "number") return value !== 0;
10149
+ return Boolean(value);
10150
+ }
10151
+ function text(value) {
10152
+ return stringify(value);
10153
+ }
10154
+ function stringify(value) {
10155
+ if (typeof value === "string") return value;
10156
+ if (typeof value === "boolean") return value ? "true" : "false";
10157
+ if (value === null || value === void 0) return "";
10158
+ if (typeof value === "number" || typeof value === "bigint") return String(value);
10159
+ try {
10160
+ return JSON.stringify(value) ?? "";
10161
+ } catch {
10162
+ return "";
10163
+ }
10164
+ }
10165
+
10166
+ // src/registry/logic.ts
10167
+ function configOf(node) {
10168
+ return node?.data?.config ?? {};
10169
+ }
10170
+ function contextOf(inputs) {
10171
+ return inputs;
10172
+ }
10173
+ function resolve(value, inputs) {
10174
+ if (typeof value !== "string") return value;
10175
+ return evaluateExpression(value, contextOf(inputs));
10176
+ }
10177
+ function conditionHolds(row, inputs) {
10178
+ const left = resolve(row.left, inputs);
10179
+ const right = resolve(row.right, inputs);
10180
+ const operator = typeof row.operator === "string" ? row.operator : "eq";
10181
+ const asText = (v) => text(v);
10182
+ const asNumber = (v) => {
10183
+ const n = typeof v === "number" ? v : Number(asText(v));
10184
+ return Number.isFinite(n) ? n : NaN;
10185
+ };
10186
+ const numeric = (fn) => {
10187
+ const a = asNumber(left);
10188
+ const b = asNumber(right);
10189
+ return Number.isFinite(a) && Number.isFinite(b) ? fn(a, b) : false;
10190
+ };
10191
+ 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;
10192
+ switch (operator) {
10193
+ // Compared as TEXT, deliberately. `right` is a text field in the schema, so
10194
+ // `status == "2"` must match the number 2 an upstream node produced —
10195
+ // strict equality there would fail on a difference the author cannot see.
10196
+ case "eq":
10197
+ return asText(left) === asText(right);
10198
+ case "neq":
10199
+ return asText(left) !== asText(right);
10200
+ case "contains":
10201
+ return asText(left).includes(asText(right));
10202
+ case "not_contains":
10203
+ return !asText(left).includes(asText(right));
10204
+ case "gt":
10205
+ return numeric((a, b) => a > b);
10206
+ case "gte":
10207
+ return numeric((a, b) => a >= b);
10208
+ case "lt":
10209
+ return numeric((a, b) => a < b);
10210
+ case "lte":
10211
+ return numeric((a, b) => a <= b);
10212
+ case "truthy":
10213
+ return truthy(left);
10214
+ case "falsy":
10215
+ return !truthy(left);
10216
+ case "empty":
10217
+ return isEmpty(left);
10218
+ case "not_empty":
10219
+ return !isEmpty(left);
10220
+ default:
10221
+ return false;
10222
+ }
10223
+ }
10224
+ var branchExecutor = (ctx) => {
10225
+ const config = configOf(ctx.node);
10226
+ const inputs = ctx.inputs;
10227
+ let taken;
10228
+ const raw = config.condition;
10229
+ if (typeof raw === "string" && raw.trim() !== "") {
10230
+ taken = truthy(resolve(raw, inputs));
10231
+ } else {
10232
+ const rows = Array.isArray(config.conditions) ? config.conditions : [];
10233
+ if (rows.length === 0) {
10234
+ taken = false;
10235
+ } else {
10236
+ const results = rows.map((row) => conditionHolds(row, inputs));
10237
+ taken = config.match === "any" ? results.some(Boolean) : results.every(Boolean);
10238
+ }
10239
+ }
10240
+ return { __port: taken ? "true" : "false", value: ctx.inputs.in ?? ctx.inputs };
10241
+ };
10242
+ var transformExecutor = (ctx) => {
10243
+ const config = configOf(ctx.node);
10244
+ const inputs = ctx.inputs;
10245
+ const passthrough = ctx.inputs.in ?? ctx.inputs;
10246
+ if (config.mode === "expression") {
10247
+ const expression = config.expression;
10248
+ if (typeof expression !== "string" || expression.trim() === "") return passthrough;
10249
+ return resolve(expression, inputs);
10250
+ }
10251
+ const rows = Array.isArray(config.fields) ? config.fields : [];
10252
+ if (rows.length === 0) return passthrough;
10253
+ const out = {};
10254
+ let wrote = false;
10255
+ for (const row of rows) {
10256
+ const key = typeof row.key === "string" ? row.key.trim() : "";
10257
+ if (key === "") continue;
10258
+ out[key] = resolve(row.value, inputs);
10259
+ wrote = true;
10260
+ }
10261
+ return wrote ? out : passthrough;
10262
+ };
10263
+ var mergeExecutor = (ctx) => {
10264
+ const entries = Object.entries(ctx.inputs);
10265
+ if (configOf(ctx.node).mode === "concat") {
10266
+ const out = [];
10267
+ for (const [, value] of entries) {
10268
+ if (value === null || value === void 0) continue;
10269
+ if (Array.isArray(value)) out.push(...value);
10270
+ else out.push(value);
10271
+ }
10272
+ return out;
10273
+ }
10274
+ const merged = {};
10275
+ for (const [port, value] of entries) {
10276
+ if (value === null || value === void 0) continue;
10277
+ if (typeof value === "object" && !Array.isArray(value)) Object.assign(merged, value);
10278
+ else merged[port] = value;
10279
+ }
10280
+ return merged;
10281
+ };
10282
+ var forEachExecutor = (ctx) => {
10283
+ const source = resolve(configOf(ctx.node).source, ctx.inputs);
10284
+ let items;
10285
+ if (Array.isArray(source)) items = source;
10286
+ else if (source === null || source === void 0) items = [];
10287
+ else if (typeof source === "object") items = Object.values(source);
10288
+ else items = [source];
10289
+ return { items, count: items.length };
10290
+ };
10291
+
10292
+ // src/registry/terminal-nodes.ts
10293
+ var DEFAULT_TIMEOUT_MS = 12e4;
10294
+ var ENTER = "\r";
10295
+ function configOf2(node) {
10296
+ return node?.data?.config ?? {};
10297
+ }
10298
+ function text2(config, key) {
10299
+ const value = config[key];
10300
+ return typeof value === "string" ? value : "";
10301
+ }
10302
+ function millis(config, key, fallback) {
10303
+ const value = Number(config[key]);
10304
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
10305
+ }
10306
+ function exitMarker(nonce) {
10307
+ const token = `__fancy_flow_exit_${nonce}__`;
10308
+ return {
10309
+ token,
10310
+ // The expanded status is required to be digits. The shell ECHOES the typed
10311
+ // command back first, and that echo contains the marker with `$?`
10312
+ // unexpanded — so requiring digits is what stops the node matching its own
10313
+ // command line and reporting success before anything has run.
10314
+ pattern: new RegExp(`${token}:(\\d+)`)
10315
+ };
10316
+ }
10317
+ var counter = 0;
10318
+ function newNonce() {
10319
+ counter += 1;
10320
+ return `${Date.now().toString(36)}${counter.toString(36)}${Math.random().toString(36).slice(2, 8)}`;
10321
+ }
10322
+ function withoutMarker(output, token) {
10323
+ return output.split("\n").filter((line) => !line.includes(token)).join("\n").trim();
10324
+ }
10325
+ function fail(ctx, message) {
10326
+ ctx.abort(message);
10327
+ throw new Error(message);
10328
+ }
10329
+ function requireTerminal(ctx) {
10330
+ if (!ctx.terminal) {
10331
+ fail(
10332
+ ctx,
10333
+ `"${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.`
10334
+ );
10335
+ }
10336
+ return ctx.terminal;
10337
+ }
10338
+ function compilePattern(ctx, raw, config) {
10339
+ const flags = typeof config.flags === "string" ? config.flags.replace(/[gy]/g, "") : "";
10340
+ try {
10341
+ return config.mode === "regex" ? new RegExp(raw, flags) : new RegExp(raw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), flags);
10342
+ } catch (e) {
10343
+ return fail(ctx, `terminal_await has an invalid regex: ${e instanceof Error ? e.message : String(e)}`);
10344
+ }
10345
+ }
10346
+ var terminalRunExecutor = async (ctx) => {
10347
+ const config = configOf2(ctx.node);
10348
+ const command = text2(config, "command").trim();
10349
+ if (!command) fail(ctx, "terminal_run has no command configured");
10350
+ const terminal = requireTerminal(ctx);
10351
+ const session = await terminal.session();
10352
+ const transcript = await terminal.transcript();
10353
+ const { token, pattern } = exitMarker(newNonce());
10354
+ const timeoutMs = millis(config, "timeoutMs", DEFAULT_TIMEOUT_MS);
10355
+ transcript.clear();
10356
+ await session.write(`${command}; printf '${token}:%s\\n' "$?"${ENTER}`);
10357
+ const result = await transcript.waitFor(pattern, { timeoutMs, exited: session.exited });
10358
+ if (result.status === "exited") {
10359
+ fail(
10360
+ ctx,
10361
+ `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.`
10362
+ );
10363
+ }
10364
+ if (result.status === "timeout") {
10365
+ fail(
10366
+ ctx,
10367
+ `"${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.`
10368
+ );
10369
+ }
10370
+ const exitCode = Number(result.match[1]);
10371
+ const output = withoutMarker(result.text, token);
10372
+ const failOnNonZero = config.failOnNonZero !== false;
10373
+ if (failOnNonZero && exitCode !== 0) {
10374
+ ctx.abort(`"${command}" exited ${exitCode}.
10375
+ ${output}`);
10376
+ }
10377
+ return { output, exitCode, command };
10378
+ };
10379
+ var terminalSendExecutor = async (ctx) => {
10380
+ const config = configOf2(ctx.node);
10381
+ const terminal = requireTerminal(ctx);
10382
+ const session = await terminal.session();
10383
+ const body = text2(config, "text");
10384
+ const submit = config.submit !== false;
10385
+ if (config.clearFirst === true) {
10386
+ (await terminal.transcript()).clear();
10387
+ }
10388
+ await session.write(submit ? `${body}${ENTER}` : body);
10389
+ return { sent: body, submitted: submit };
10390
+ };
10391
+ var terminalAwaitExecutor = async (ctx) => {
10392
+ const config = configOf2(ctx.node);
10393
+ const terminal = requireTerminal(ctx);
10394
+ const session = await terminal.session();
10395
+ const transcript = await terminal.transcript();
10396
+ const raw = text2(config, "pattern").trim();
10397
+ if (!raw) fail(ctx, "terminal_await has no pattern configured \u2014 there is nothing for it to wait for");
10398
+ const pattern = compilePattern(ctx, raw, config);
10399
+ const timeoutMs = millis(config, "timeoutMs", DEFAULT_TIMEOUT_MS);
10400
+ const result = await transcript.waitFor(pattern, {
10401
+ timeoutMs,
10402
+ exited: session.exited
10403
+ });
10404
+ if (result.status === "exited") {
10405
+ fail(
10406
+ ctx,
10407
+ `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.`
10408
+ );
10409
+ }
10410
+ if (result.status === "timeout") {
10411
+ if (config.onTimeout === "continue") {
10412
+ ctx.emit({
10413
+ type: "log",
10414
+ nodeId: ctx.node.id,
10415
+ level: "warn",
10416
+ message: `terminal_await: ${JSON.stringify(raw)} did not appear within ${timeoutMs}ms; continuing as configured.`
10417
+ });
10418
+ return { matched: false, output: result.text, groups: [] };
10419
+ }
10420
+ fail(
10421
+ ctx,
10422
+ `${JSON.stringify(raw)} did not appear within ${timeoutMs}ms. Last output:
10423
+ ${result.text.slice(-2e3)}`
10424
+ );
10425
+ }
10426
+ return {
10427
+ matched: true,
10428
+ output: result.text,
10429
+ matchedText: result.match[0],
10430
+ // Capture groups are the reason to use regex mode at all — a prompt that
10431
+ // reports a session id or a file path is only useful if the value comes out.
10432
+ groups: result.match.slice(1)
10433
+ };
10434
+ };
10435
+
9820
10436
  // src/registry/builtin.ts
9821
10437
  var HUMAN_FIELD_OUTPUT_TYPE = {
9822
10438
  text: "string",
@@ -10106,6 +10722,9 @@ var KINDS = [
10106
10722
  // returns its input on the chosen port
10107
10723
  emits: "input",
10108
10724
  aliases: ["branch", "@fancy/branch"],
10725
+ // Pure logic: no I/O to choose, so a default is safe. A host
10726
+ // executor still wins -- pickExecutor consults the registry first.
10727
+ executor: branchExecutor,
10109
10728
  category: "logic",
10110
10729
  label: "Branch",
10111
10730
  description: "Multi-way branch on a condition or value.",
@@ -10203,6 +10822,9 @@ var KINDS = [
10203
10822
  { path: "count", type: "number" }
10204
10823
  ],
10205
10824
  aliases: ["for_each", "@fancy/for_each"],
10825
+ // Pure logic: no I/O to choose, so a default is safe. A host
10826
+ // executor still wins -- pickExecutor consults the registry first.
10827
+ executor: forEachExecutor,
10206
10828
  category: "logic",
10207
10829
  label: "For Each",
10208
10830
  description: "Iterate over a list, emitting each item on `item`.",
@@ -10223,6 +10845,9 @@ var KINDS = [
10223
10845
  // reference.
10224
10846
  emits: (config) => (config?.mode ?? "merge") === "concat" ? null : "inputs-merged",
10225
10847
  aliases: ["merge", "@fancy/merge"],
10848
+ // Pure logic: no I/O to choose, so a default is safe. A host
10849
+ // executor still wins -- pickExecutor consults the registry first.
10850
+ executor: mergeExecutor,
10226
10851
  category: "logic",
10227
10852
  label: "Merge",
10228
10853
  description: "Combine multiple inputs into one object or array.",
@@ -10268,6 +10893,9 @@ var KINDS = [
10268
10893
  // else the shape that expression names. So the RELATION is config-dependent.
10269
10894
  emits: (config) => (config?.expression ?? "") === "" ? "input" : "expression:expression",
10270
10895
  aliases: ["transform", "@fancy/transform"],
10896
+ // Pure logic: no I/O to choose, so a default is safe. A host
10897
+ // executor still wins -- pickExecutor consults the registry first.
10898
+ executor: transformExecutor,
10271
10899
  category: "logic",
10272
10900
  label: "Transform",
10273
10901
  description: "Reshape data with an expression.",
@@ -10726,6 +11354,179 @@ var KINDS = [
10726
11354
  resizable: { minWidth: 160, minHeight: 72 },
10727
11355
  component: LaneNode
10728
11356
  },
11357
+ {
11358
+ // Terminal lane — a lane that OWNS a terminal for the length of the run.
11359
+ //
11360
+ // Visually a swimlane, and `layout` for the same reason the plain lane is:
11361
+ // a lane is not a step, so it never executes and never appears in topo
11362
+ // order. What is new is that it DECLARES a resource. The runtime opens its
11363
+ // terminal lazily, when the first terminal node inside it runs, and closes
11364
+ // it in the run's `finally` — so a graph that never reaches a terminal node
11365
+ // never spawns a process, and one that does gets exactly one session no
11366
+ // matter how many nodes use it.
11367
+ //
11368
+ // Membership is `parentId`, which already persists into the WorkflowSchema.
11369
+ // That is what lets a headless runtime resolve the same grouping the canvas
11370
+ // shows, without a second association to keep in sync.
11371
+ name: "@particle-academy/terminal_lane",
11372
+ aliases: ["terminal_lane", "@fancy/terminal_lane"],
11373
+ category: "layout",
11374
+ label: "Terminal lane",
11375
+ description: "A lane that owns one terminal. Opens at the first terminal node, closes when the run ends.",
11376
+ icon: "\u25A3",
11377
+ inputs: [],
11378
+ outputs: [],
11379
+ configSchema: [
11380
+ { type: "text", key: "title", label: "Title", default: "Terminal" },
11381
+ {
11382
+ type: "text",
11383
+ key: "command",
11384
+ label: "Command",
11385
+ placeholder: "Leave empty for the default shell"
11386
+ },
11387
+ { type: "text", key: "cwd", label: "Working directory", placeholder: "Host default" },
11388
+ { type: "keyvalue", key: "env", label: "Environment" },
11389
+ {
11390
+ type: "select",
11391
+ key: "orientation",
11392
+ label: "Orientation",
11393
+ default: "horizontal",
11394
+ options: [
11395
+ { value: "horizontal", label: "Row" },
11396
+ { value: "vertical", label: "Column" }
11397
+ ]
11398
+ }
11399
+ ],
11400
+ defaultConfig: { title: "Terminal", orientation: "horizontal" },
11401
+ resizable: { minWidth: 220, minHeight: 120 },
11402
+ component: LaneNode
11403
+ },
11404
+ {
11405
+ // Terminal nodes. All three live inside a terminal lane and talk to the
11406
+ // session that lane owns; outside one they abort by name rather than
11407
+ // opening a shell of their own.
11408
+ name: "@particle-academy/terminal_run",
11409
+ aliases: ["terminal_run", "@fancy/terminal_run"],
11410
+ category: "io",
11411
+ label: "Run in terminal",
11412
+ description: "Run a shell command in the lane's terminal and wait for its exit code.",
11413
+ icon: "$",
11414
+ inputs: [{ id: "in" }],
11415
+ outputs: [{ id: "out", label: "output" }],
11416
+ executor: terminalRunExecutor,
11417
+ configSchema: [
11418
+ {
11419
+ type: "textarea",
11420
+ key: "command",
11421
+ label: "Command",
11422
+ rows: 3,
11423
+ required: true,
11424
+ placeholder: "npm test",
11425
+ 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."
11426
+ },
11427
+ {
11428
+ type: "number",
11429
+ key: "timeoutMs",
11430
+ label: "Timeout (ms)",
11431
+ default: 12e4,
11432
+ description: "How long to wait for the command to finish before failing the run."
11433
+ },
11434
+ {
11435
+ type: "switch",
11436
+ key: "failOnNonZero",
11437
+ label: "Fail the run on a non-zero exit",
11438
+ default: true,
11439
+ 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."
11440
+ }
11441
+ ],
11442
+ defaultConfig: { command: "", timeoutMs: 12e4, failOnNonZero: true }
11443
+ },
11444
+ {
11445
+ name: "@particle-academy/terminal_send",
11446
+ aliases: ["terminal_send", "@fancy/terminal_send"],
11447
+ category: "io",
11448
+ label: "Send to terminal",
11449
+ description: "Type text at whatever is running in the lane's terminal, without waiting.",
11450
+ icon: "\u2328",
11451
+ inputs: [{ id: "in" }],
11452
+ outputs: [{ id: "out", label: "sent" }],
11453
+ executor: terminalSendExecutor,
11454
+ configSchema: [
11455
+ {
11456
+ type: "textarea",
11457
+ key: "text",
11458
+ label: "Text",
11459
+ rows: 4,
11460
+ placeholder: "Summarise the failing test and propose a fix.",
11461
+ description: "What to type. This is how a graph prompts an agent TUI such as Claude Code or Codex."
11462
+ },
11463
+ {
11464
+ type: "switch",
11465
+ key: "submit",
11466
+ label: "Press Enter",
11467
+ default: true,
11468
+ description: "Off leaves the text on the input line \u2014 useful for building one up across several nodes."
11469
+ },
11470
+ {
11471
+ type: "switch",
11472
+ key: "clearFirst",
11473
+ label: "Forget earlier output first",
11474
+ default: false,
11475
+ description: "Discards anything the terminal said before this send, so a following Await cannot match a prompt left over from the previous exchange."
11476
+ }
11477
+ ],
11478
+ defaultConfig: { text: "", submit: true, clearFirst: false }
11479
+ },
11480
+ {
11481
+ name: "@particle-academy/terminal_await",
11482
+ aliases: ["terminal_await", "@fancy/terminal_await"],
11483
+ category: "io",
11484
+ label: "Await terminal output",
11485
+ description: "Wait until the lane's terminal prints something that matches.",
11486
+ icon: "\u23F1",
11487
+ inputs: [{ id: "in" }],
11488
+ outputs: [{ id: "out", label: "output" }],
11489
+ executor: terminalAwaitExecutor,
11490
+ configSchema: [
11491
+ {
11492
+ type: "text",
11493
+ key: "pattern",
11494
+ label: "Wait for",
11495
+ required: true,
11496
+ placeholder: "esc to interrupt",
11497
+ description: "Matched against the output with colour codes already stripped, so match what you SEE."
11498
+ },
11499
+ {
11500
+ type: "select",
11501
+ key: "mode",
11502
+ label: "Match as",
11503
+ default: "text",
11504
+ options: [
11505
+ { value: "text", label: "Plain text" },
11506
+ { value: "regex", label: "Regular expression" }
11507
+ ],
11508
+ description: "Regex mode also returns capture groups, which is how a value gets out of a prompt."
11509
+ },
11510
+ {
11511
+ type: "number",
11512
+ key: "timeoutMs",
11513
+ label: "Timeout (ms)",
11514
+ default: 12e4
11515
+ },
11516
+ {
11517
+ type: "select",
11518
+ key: "onTimeout",
11519
+ label: "If it never appears",
11520
+ default: "fail",
11521
+ options: [
11522
+ { value: "fail", label: "Fail the run" },
11523
+ { value: "continue", label: "Continue with matched: false" }
11524
+ ],
11525
+ 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."
11526
+ }
11527
+ ],
11528
+ defaultConfig: { pattern: "", mode: "text", timeoutMs: 12e4, onTimeout: "fail" }
11529
+ },
10729
11530
  {
10730
11531
  // Note — a sticky-note annotation. Portless + visual-only: the runtime skips
10731
11532
  // the `annotation` category, so a note's text NEVER reaches a runner — it