@particle-academy/fancy-flow 0.65.2 → 0.66.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) 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-27RZJAE2.js +2586 -0
  7. package/dist/chunk-27RZJAE2.js.map +1 -0
  8. package/dist/{chunk-MBVX4ZRB.js → chunk-2QTEELJE.js} +4 -3
  9. package/dist/chunk-2QTEELJE.js.map +1 -0
  10. package/dist/chunk-2QXKDLGT.js +58 -0
  11. package/dist/chunk-2QXKDLGT.js.map +1 -0
  12. package/dist/{chunk-W5DPKJY4.js → chunk-6AYC3BWI.js} +4 -4
  13. package/dist/chunk-6AYC3BWI.js.map +1 -0
  14. package/dist/{chunk-5H54OTKT.js → chunk-6TH646CS.js} +127 -5
  15. package/dist/chunk-6TH646CS.js.map +1 -0
  16. package/dist/{chunk-EO6444T2.js → chunk-BUNFPBTF.js} +5 -4
  17. package/dist/chunk-BUNFPBTF.js.map +1 -0
  18. package/dist/{chunk-77V4QC6Y.js → chunk-FV3D366L.js} +3 -3
  19. package/dist/{chunk-77V4QC6Y.js.map → chunk-FV3D366L.js.map} +1 -1
  20. package/dist/{chunk-JF6WCRBU.js → chunk-OWENS2H5.js} +36 -1905
  21. package/dist/chunk-OWENS2H5.js.map +1 -0
  22. package/dist/{chunk-RIAFHQT5.js → chunk-RE3XNVSB.js} +3 -3
  23. package/dist/{chunk-RIAFHQT5.js.map → chunk-RE3XNVSB.js.map} +1 -1
  24. package/dist/connectors.d.cts +2 -2
  25. package/dist/connectors.d.ts +2 -2
  26. package/dist/durable/index.d.cts +2 -2
  27. package/dist/durable/index.d.ts +2 -2
  28. package/dist/durable.cjs +865 -9135
  29. package/dist/durable.cjs.map +1 -1
  30. package/dist/durable.js +2 -4
  31. package/dist/durable.js.map +1 -1
  32. package/dist/engine.cjs +927 -9278
  33. package/dist/engine.cjs.map +1 -1
  34. package/dist/engine.d.cts +6 -7
  35. package/dist/engine.d.ts +6 -7
  36. package/dist/engine.js +5 -7
  37. package/dist/engine.js.map +1 -1
  38. package/dist/fields/react-fancy.d.cts +3 -3
  39. package/dist/fields/react-fancy.d.ts +3 -3
  40. package/dist/index.cjs +11009 -10282
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +65 -36
  43. package/dist/index.d.ts +65 -36
  44. package/dist/index.js +22 -19
  45. package/dist/index.js.map +1 -1
  46. package/dist/layout/index.d.cts +1 -1
  47. package/dist/layout/index.d.ts +1 -1
  48. package/dist/llm/prism.cjs.map +1 -1
  49. package/dist/llm/prism.d.cts +1 -2
  50. package/dist/llm/prism.d.ts +1 -2
  51. package/dist/llm/prism.js +1 -1
  52. package/dist/llm/vercel-ai.cjs.map +1 -1
  53. package/dist/llm/vercel-ai.d.cts +1 -2
  54. package/dist/llm/vercel-ai.d.ts +1 -2
  55. package/dist/llm/vercel-ai.js +1 -1
  56. package/dist/registry/index.d.cts +20 -9
  57. package/dist/registry/index.d.ts +20 -9
  58. package/dist/{registry-CntAdUcY.d.cts → registry-BcgllSk2.d.cts} +1 -1
  59. package/dist/{registry-DYoV-MQi.d.ts → registry-h-2QswZ5.d.ts} +1 -1
  60. package/dist/registry.cjs +11252 -10409
  61. package/dist/registry.cjs.map +1 -1
  62. package/dist/registry.js +5 -4
  63. package/dist/{run-cohort-C1nRh_mi.d.ts → run-cohort-CjUBWg6X.d.ts} +2 -2
  64. package/dist/{run-cohort-CBRAUj-z.d.cts → run-cohort-DL9JzzPU.d.cts} +2 -2
  65. package/dist/{run-flow-2XsHBwpd.d.cts → run-flow-B_8hgO5_.d.cts} +1 -1
  66. package/dist/{run-flow-D7AOCcfE.d.ts → run-flow-CxEGBOxd.d.ts} +1 -1
  67. package/dist/runtime/index.d.cts +5 -5
  68. package/dist/runtime/index.d.ts +5 -5
  69. package/dist/runtime.cjs +10843 -10194
  70. package/dist/runtime.cjs.map +1 -1
  71. package/dist/runtime.js +5 -5
  72. package/dist/schema/index.d.cts +19 -6
  73. package/dist/schema/index.d.ts +19 -6
  74. package/dist/schema.cjs +881 -9142
  75. package/dist/schema.cjs.map +1 -1
  76. package/dist/schema.js +4 -5
  77. package/dist/screens.cjs +842 -86
  78. package/dist/screens.cjs.map +1 -1
  79. package/dist/screens.d.cts +2 -2
  80. package/dist/screens.d.ts +2 -2
  81. package/dist/screens.js +7 -6
  82. package/dist/screens.js.map +1 -1
  83. package/dist/terminal/fancy-term-host.cjs +99 -0
  84. package/dist/terminal/fancy-term-host.cjs.map +1 -0
  85. package/dist/terminal/fancy-term-host.d.cts +76 -0
  86. package/dist/terminal/fancy-term-host.d.ts +76 -0
  87. package/dist/terminal/fancy-term-host.js +92 -0
  88. package/dist/terminal/fancy-term-host.js.map +1 -0
  89. package/dist/types-B-Syk9-M.d.cts +620 -0
  90. package/dist/types-B-Syk9-M.d.ts +620 -0
  91. package/dist/{types-D71SKA5A.d.cts → types-C2ATTmjN.d.cts} +1 -1
  92. package/dist/{types-Ckhz-YwC.d.ts → types-VunLGqrn.d.ts} +1 -1
  93. package/dist/ux.cjs +885 -9142
  94. package/dist/ux.cjs.map +1 -1
  95. package/dist/ux.d.cts +2 -2
  96. package/dist/ux.d.ts +2 -2
  97. package/dist/ux.js +2 -3
  98. package/dist/ux.js.map +1 -1
  99. package/package.json +11 -1
  100. package/dist/capabilities-BYa5p5jw.d.cts +0 -112
  101. package/dist/capabilities-COOXRiNL.d.ts +0 -112
  102. package/dist/chunk-5H54OTKT.js.map +0 -1
  103. package/dist/chunk-EO6444T2.js.map +0 -1
  104. package/dist/chunk-JF6WCRBU.js.map +0 -1
  105. package/dist/chunk-MBVX4ZRB.js.map +0 -1
  106. package/dist/chunk-UM4C46AF.js +0 -103
  107. package/dist/chunk-UM4C46AF.js.map +0 -1
  108. package/dist/chunk-USL4FMFU.js +0 -41
  109. package/dist/chunk-USL4FMFU.js.map +0 -1
  110. package/dist/chunk-W5DPKJY4.js.map +0 -1
  111. package/dist/types-JFYjPJAG.d.cts +0 -333
  112. package/dist/types-JFYjPJAG.d.ts +0 -333
@@ -1,6 +1,6 @@
1
- export { F as FlowNodeStatus, a as FlowViewer, b as FlowViewerClassNames, c as FlowViewerProps } from './FlowViewer-DABx-6A-.cjs';
1
+ export { F as FlowNodeStatus, a as FlowViewer, b as FlowViewerClassNames, c as FlowViewerProps } from './FlowViewer-CoAJ3o52.cjs';
2
2
  import 'react';
3
- import './types-JFYjPJAG.cjs';
3
+ import './types-B-Syk9-M.cjs';
4
4
  import '@xyflow/react';
5
5
 
6
6
  /**
package/dist/screens.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { F as FlowNodeStatus, a as FlowViewer, b as FlowViewerClassNames, c as FlowViewerProps } from './FlowViewer-BI_AyyvU.js';
1
+ export { F as FlowNodeStatus, a as FlowViewer, b as FlowViewerClassNames, c as FlowViewerProps } from './FlowViewer-BCc_BvuD.js';
2
2
  import 'react';
3
- import './types-JFYjPJAG.js';
3
+ import './types-B-Syk9-M.js';
4
4
  import '@xyflow/react';
5
5
 
6
6
  /**
package/dist/screens.js CHANGED
@@ -1,10 +1,11 @@
1
- import { FlowViewer } from './chunk-EO6444T2.js';
2
- export { FlowViewer } from './chunk-EO6444T2.js';
3
- import './chunk-5H54OTKT.js';
4
- import './chunk-UEOE6B52.js';
5
- import './chunk-JF6WCRBU.js';
1
+ import { FlowViewer } from './chunk-BUNFPBTF.js';
2
+ export { FlowViewer } from './chunk-BUNFPBTF.js';
3
+ import './chunk-6TH646CS.js';
6
4
  import './chunk-F5RPRB7A.js';
7
- import './chunk-USL4FMFU.js';
5
+ import './chunk-OWENS2H5.js';
6
+ import './chunk-UEOE6B52.js';
7
+ import './chunk-27RZJAE2.js';
8
+ import './chunk-2QXKDLGT.js';
8
9
  import { registerSchemaComponents } from '@particle-academy/fancy-screens';
9
10
 
10
11
  function registerFlowSchema() {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/screens.ts"],"names":[],"mappings":";;;;;;;;;AAwBO,SAAS,kBAAA,GAA2B;AACzC,EAAA,wBAAA,CAAyB;AAAA,IACvB;AAAA,GACD,CAAA;AACH","file":"screens.js","sourcesContent":["// /screens subpath — the ONLY module that imports fancy-screens (optional\n// peer). Keeps the base `.` import graph free of it, so an app that never\n// touches fancy-screens never pays for it.\nimport { registerSchemaComponents } from \"@particle-academy/fancy-screens\";\nimport type { ComponentType } from \"react\";\nimport { FlowViewer } from \"./components/FlowViewer\";\n\n/**\n * Register fancy-flow's components with fancy-screens, so an agent-emitted\n * `ScreenSchema` can place a workflow in a page:\n *\n * ```json\n * { \"type\": \"FlowViewer\", \"props\": { \"graph\": { \"nodes\": [], \"edges\": [] } } }\n * ```\n *\n * Call once at host startup.\n *\n * **Only the VIEWER is registered, deliberately.** A schema is JSON an agent\n * emits, and `FlowEditor` needs executors, run handlers and controlled state\n * that cannot be expressed as JSON props — registering it would let an agent\n * emit an editor that renders but does nothing, which is worse than not\n * offering it. The viewer is complete from props alone, which is exactly what\n * makes it schema-safe.\n */\nexport function registerFlowSchema(): void {\n registerSchemaComponents({\n FlowViewer: FlowViewer as unknown as ComponentType<Record<string, unknown>>,\n });\n}\n\nexport { FlowViewer } from \"./components/FlowViewer\";\nexport type { FlowViewerProps, FlowViewerClassNames, FlowNodeStatus } from \"./components/FlowViewer\";\n"]}
1
+ {"version":3,"sources":["../src/screens.ts"],"names":[],"mappings":";;;;;;;;;;AAwBO,SAAS,kBAAA,GAA2B;AACzC,EAAA,wBAAA,CAAyB;AAAA,IACvB;AAAA,GACD,CAAA;AACH","file":"screens.js","sourcesContent":["// /screens subpath — the ONLY module that imports fancy-screens (optional\n// peer). Keeps the base `.` import graph free of it, so an app that never\n// touches fancy-screens never pays for it.\nimport { registerSchemaComponents } from \"@particle-academy/fancy-screens\";\nimport type { ComponentType } from \"react\";\nimport { FlowViewer } from \"./components/FlowViewer\";\n\n/**\n * Register fancy-flow's components with fancy-screens, so an agent-emitted\n * `ScreenSchema` can place a workflow in a page:\n *\n * ```json\n * { \"type\": \"FlowViewer\", \"props\": { \"graph\": { \"nodes\": [], \"edges\": [] } } }\n * ```\n *\n * Call once at host startup.\n *\n * **Only the VIEWER is registered, deliberately.** A schema is JSON an agent\n * emits, and `FlowEditor` needs executors, run handlers and controlled state\n * that cannot be expressed as JSON props — registering it would let an agent\n * emit an editor that renders but does nothing, which is worse than not\n * offering it. The viewer is complete from props alone, which is exactly what\n * makes it schema-safe.\n */\nexport function registerFlowSchema(): void {\n registerSchemaComponents({\n FlowViewer: FlowViewer as unknown as ComponentType<Record<string, unknown>>,\n });\n}\n\nexport { FlowViewer } from \"./components/FlowViewer\";\nexport type { FlowViewerProps, FlowViewerClassNames, FlowNodeStatus } from \"./components/FlowViewer\";\n"]}
@@ -0,0 +1,99 @@
1
+ 'use strict';
2
+
3
+ // src/registry/capabilities.ts
4
+ function registerTerminalHost(host) {
5
+ return () => {
6
+ };
7
+ }
8
+
9
+ // src/terminal/fancy-term-host.ts
10
+ var fanouts = /* @__PURE__ */ new WeakMap();
11
+ function fanoutFor(backend) {
12
+ const existing = fanouts.get(backend);
13
+ if (existing) return existing;
14
+ const fanout = { data: /* @__PURE__ */ new Map(), exit: /* @__PURE__ */ new Map() };
15
+ fanouts.set(backend, fanout);
16
+ backend.on("data", (id, chunk) => {
17
+ const listeners = fanout.data.get(id);
18
+ if (!listeners) return;
19
+ for (const listener of [...listeners]) listener(chunk);
20
+ });
21
+ backend.on("exit", (id, payload) => {
22
+ fanout.exit.get(id)?.({
23
+ exitCode: payload.exitCode,
24
+ // fancy-term-host reports a signal NUMBER; `TerminalExit.signal` is a
25
+ // string, because a signal is a name everywhere a person reads one.
26
+ signal: payload.signal === void 0 ? void 0 : String(payload.signal)
27
+ });
28
+ fanout.exit.delete(id);
29
+ fanout.data.delete(id);
30
+ });
31
+ return fanout;
32
+ }
33
+ function runtimeCwd() {
34
+ const proc = globalThis.process;
35
+ return typeof proc?.cwd === "function" ? proc.cwd() : ".";
36
+ }
37
+ var counter = 0;
38
+ function defaultNewId() {
39
+ counter += 1;
40
+ return `flow-${Date.now().toString(36)}-${counter.toString(36)}`;
41
+ }
42
+ function createFancyTermHost(backend, defaults = {}) {
43
+ const fanout = fanoutFor(backend);
44
+ const newId = defaults.newId ?? defaultNewId;
45
+ return {
46
+ open: (spec) => {
47
+ const id = newId();
48
+ let settleExit = () => {
49
+ };
50
+ const exited = new Promise((resolve) => {
51
+ settleExit = resolve;
52
+ });
53
+ fanout.exit.set(id, settleExit);
54
+ const listeners = /* @__PURE__ */ new Set();
55
+ fanout.data.set(id, listeners);
56
+ const info = backend.create({
57
+ id,
58
+ // Required by `CreateTerminalOpts`, optional in a lane's config —
59
+ // `process.cwd()` is the same default a person typing in a terminal
60
+ // would get.
61
+ cwd: spec.cwd ?? defaults.cwd ?? runtimeCwd(),
62
+ shell: spec.command ?? void 0,
63
+ args: spec.args ?? defaults.args,
64
+ cols: spec.cols ?? defaults.cols,
65
+ rows: spec.rows ?? defaults.rows,
66
+ // The lane's own env wins: a default exists to fill a gap, not to
67
+ // override something an author wrote on the node.
68
+ env: { ...defaults.env, ...spec.env }
69
+ });
70
+ return {
71
+ id: info.id,
72
+ write: (data) => {
73
+ backend.write(id, data);
74
+ },
75
+ onData: (listener) => {
76
+ listeners.add(listener);
77
+ return () => {
78
+ listeners.delete(listener);
79
+ };
80
+ },
81
+ exited,
82
+ close: () => {
83
+ backend.kill(id);
84
+ fanout.data.delete(id);
85
+ settleExit({ exitCode: 0 });
86
+ fanout.exit.delete(id);
87
+ }
88
+ };
89
+ }
90
+ };
91
+ }
92
+ function useFancyTermHost(backend, defaults = {}) {
93
+ return registerTerminalHost(createFancyTermHost(backend, defaults));
94
+ }
95
+
96
+ exports.createFancyTermHost = createFancyTermHost;
97
+ exports.useFancyTermHost = useFancyTermHost;
98
+ //# sourceMappingURL=fancy-term-host.cjs.map
99
+ //# sourceMappingURL=fancy-term-host.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/registry/capabilities.ts","../../src/terminal/fancy-term-host.ts"],"names":[],"mappings":"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","file":"fancy-term-host.cjs","sourcesContent":["import type { FlowGraph } from \"../types\";\n\n/**\n * Host capabilities — the services core nodes need but must never depend on.\n *\n * A node that imports a provider SDK forces every consumer to install it: a\n * workflow app that never calls a model should not inherit an LLM dependency.\n * So core declares the CONTRACT and the host supplies the implementation, the\n * same arrangement `renderDocumentField` already uses for documents.\n *\n * That keeps opinionated nodes in core without their opinions: `llm_branch`\n * ships the routing semantics, port derivation and config UI, while whichever\n * client the host registers — Prism, an OpenAI SDK, a local model, a fake in a\n * test — decides how the question actually gets asked.\n *\n * Registration is deliberately explicit and typed per capability rather than a\n * stringly-keyed bag, so a missing one is a clear error at the seam instead of\n * an undefined somewhere downstream.\n */\n\n// ── LLM ─────────────────────────────────────────────────────────────────────\n\nexport type LlmRoute = { port: string; description?: string };\n\nexport type LlmRouteRequest = {\n /** Optional framing for the decision. */\n system?: string;\n /** What the model is deciding about. */\n prompt: string;\n /** The ports it must choose between. */\n routes: LlmRoute[];\n provider?: string;\n model?: string;\n /** Host-resolved credential reference, never a raw key. */\n credential?: string;\n};\n\nexport type LlmRouteChoice = {\n /** Must be one of the requested route ports. */\n port: string;\n /** Why — carried down the chosen port so a run is explainable afterwards. */\n reason?: string;\n};\n\n/**\n * The only thing core asks of an LLM: given routes, pick one.\n *\n * Deliberately not a general chat interface. A narrow contract is one a host\n * can satisfy in a few lines over any SDK, and it keeps the choice\n * machine-checkable — an implementation should constrain the model to the\n * declared ports (structured output / enum) rather than parsing prose.\n */\nexport type LlmClient = {\n chooseRoute: (request: LlmRouteRequest) => Promise<LlmRouteChoice> | LlmRouteChoice;\n};\n\nlet llmClient: LlmClient | null = null;\n\n/** Install the host's LLM client. Returns an unregister function. */\nexport function registerLlmClient(client: LlmClient): () => void {\n llmClient = client;\n return () => {\n if (llmClient === client) llmClient = null;\n };\n}\n\nexport function getLlmClient(): LlmClient | null {\n return llmClient;\n}\n\n// ── Terminal ────────────────────────────────────────────────────────────────\n\n/** What to launch. Omit `command` for the host's default shell. */\nexport type TerminalSessionSpec = {\n command?: string;\n args?: string[];\n cwd?: string;\n env?: Record<string, string>;\n cols?: number;\n rows?: number;\n};\n\nexport type TerminalExit = { exitCode: number; signal?: string };\n\n/**\n * One live terminal, owned by the host for as long as the run needs it.\n *\n * Output arrives by SUBSCRIPTION rather than a `read()` the engine calls. A TUI\n * emits continuously and on its own schedule — repainting, streaming a reply,\n * redrawing a spinner — so a polled read either misses bytes between calls or\n * has to buffer them anyway, and every host would invent that buffer\n * differently.\n */\nexport type TerminalSession = {\n id: string;\n write: (data: string) => void | Promise<void>;\n /** Subscribe to output. Returns an unsubscribe. */\n onData: (listener: (chunk: string) => void) => () => void;\n /**\n * Resolves when the process exits.\n *\n * A promise rather than an `exited` flag the engine polls, so \"the agent\n * quit\" and \"the agent has not answered yet\" are distinguishable while\n * waiting. Without it a node awaiting output cannot tell a slow reply from a\n * dead process, and waits out its timeout either way.\n */\n exited: Promise<TerminalExit>;\n close: () => void | Promise<void>;\n};\n\n/**\n * The only thing core asks of a terminal: open one.\n *\n * ## Why the contract stops at the PTY\n *\n * There is deliberately no `waitForOutput(pattern)` here, though every terminal\n * node needs one. Matching is derivable from `onData`, so putting it in the\n * contract would mean every host implements it — and two implementations of one\n * agreed rule is precisely how a week of registry defects happened: both sides\n * agreed output matching should work, and would have disagreed on whether a\n * pattern spans chunk boundaries, whether ANSI escapes are stripped first, and\n * what a timeout returns.\n *\n * So core owns the matching and the host owns the process. A host satisfies\n * this in a few lines over `node-pty`; nothing about how a run interprets the\n * bytes is up to it.\n *\n * ## Why this exists at all\n *\n * A terminal cannot live in the engine. `node-pty` is a native addon, so\n * importing it would break every browser build of this package and force the\n * dependency on consumers who never run a terminal node — the same reason the\n * LLM client is a contract rather than an SDK import. The desktop app that CAN\n * spawn a PTY registers one; everyone else never notices the capability exists.\n */\nexport type TerminalHost = {\n open: (spec: TerminalSessionSpec) => Promise<TerminalSession> | TerminalSession;\n};\n\nlet terminalHost: TerminalHost | null = null;\n\n/** Install the host's terminal. Returns an unregister function. */\nexport function registerTerminalHost(host: TerminalHost): () => void {\n terminalHost = host;\n return () => {\n if (terminalHost === host) terminalHost = null;\n };\n}\n\nexport function getTerminalHost(): TerminalHost | null {\n return terminalHost;\n}\n\n// ── Workflow resolution ─────────────────────────────────────────────────────\n\n/**\n * Why a workflow reference could not be resolved.\n *\n * `missing` and `version-mismatch` are deliberately distinct. Collapsing them\n * into a bare null makes \"no such workflow\" indistinguishable from \"that\n * workflow exists, but it is not the one you pinned\" — and the second wants an\n * error naming both versions, because it is the interesting failure.\n */\nexport type WorkflowResolutionFailure = {\n reason: \"missing\" | \"version-mismatch\";\n /** The version the host actually holds, when it holds one. */\n available?: number;\n message?: string;\n};\n\nexport type WorkflowResolution = FlowGraph | WorkflowResolutionFailure | null;\n\n/**\n * Resolve a workflow reference to a runnable graph.\n *\n * `subflow` names another workflow rather than embedding it, so the host owns\n * where workflows live — a database, a file, an API.\n *\n * ## Why `version` is here\n *\n * A workflow another workflow depends on is an INTERFACE, and interfaces need\n * pins. Without a version, a parent goes on calling `invoice-triage`, someone\n * edits that child, and the parent now runs different logic having reported\n * success the whole time — correct-looking, no error, wrong behaviour. The same\n * failure family as the 0.9.0 routing divergence.\n *\n * The parameter lives on the resolver rather than being encoded into the ref\n * string (`invoice-triage@3`) because a stringly-typed protocol is one every\n * host invents differently — the \"three vocabularies for one node\" problem.\n *\n * Raised by the MOIC Suite consumer, whose `workflow_ref` pins versions and\n * fails loudly on mismatch. Their point: a host COULD NOT implement pinning\n * before this, because the node had no way to ask and the resolver no way to\n * receive.\n *\n * Returning `null` still means \"no such workflow\". Return a\n * {@link WorkflowResolutionFailure} to distinguish a version mismatch.\n */\nexport type WorkflowResolver = (\n ref: string,\n version?: number,\n) => Promise<WorkflowResolution> | WorkflowResolution;\n\n/** Narrow a resolver's return value to an explicit failure. */\nexport function isResolutionFailure(value: WorkflowResolution): value is WorkflowResolutionFailure {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"reason\" in value &&\n (value as WorkflowResolutionFailure).reason !== undefined\n );\n}\n\nlet workflowResolver: WorkflowResolver | null = null;\n\n/** Install the host's workflow resolver. Returns an unregister function. */\nexport function registerWorkflowResolver(resolver: WorkflowResolver): () => void {\n workflowResolver = resolver;\n return () => {\n if (workflowResolver === resolver) workflowResolver = null;\n };\n}\n\nexport function getWorkflowResolver(): WorkflowResolver | null {\n return workflowResolver;\n}\n\n// ── Introspection ───────────────────────────────────────────────────────────\n\nexport type CapabilityId = \"llm\" | \"workflow_resolver\" | \"document\" | \"terminal\";\n\n/**\n * Which capabilities are currently satisfied.\n *\n * Exists so a host (or the CLI, or an agent over MCP) can answer \"what does\n * this graph need that I haven't wired?\" BEFORE a run fails halfway through.\n */\nexport function capabilityStatus(): Record<CapabilityId, boolean> {\n // Imported lazily to avoid dragging the React-dependent rich-input module\n // into the headless engine.\n let documentReady = false;\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires, no-undef\n documentReady = Boolean((globalThis as any).__fancyFlowDocumentAdapter);\n } catch {\n documentReady = false;\n }\n return {\n llm: llmClient !== null,\n workflow_resolver: workflowResolver !== null,\n document: documentReady,\n // Listed here for the reason this function exists at all. A graph with a\n // terminal lane and no host registered fails PART WAY THROUGH — after the\n // nodes before it have already run and possibly written somewhere — and the\n // whole point of asking up front is to learn that before starting. Leaving\n // it out would have made the one check designed to catch this report\n // all-clear for exactly the case it was built for.\n terminal: terminalHost !== null,\n };\n}\n","/**\n * A `TerminalHost` backed by `@particle-academy/fancy-term-host`.\n *\n * ## Why this takes the backend as an argument instead of importing it\n *\n * `fancy-term-host` declares `node-pty` as a REQUIRED peer, and its `manager.ts`\n * imports it at module scope — so importing the package at all pulls a native\n * addon in at load time. Depending on it here, even as a devDependency for the\n * types, would put a native build in fancy-flow's own CI install on every\n * platform, for a package that does not otherwise need one.\n *\n * The consumer who wants this adapter is a desktop app that has already wired\n * `fancy-term-host` and holds a live backend. Handing it in costs them one\n * argument and costs everyone else nothing:\n *\n * ```ts\n * import { terminalManager } from \"@particle-academy/fancy-term-host\";\n * import { useFancyTermHost } from \"@particle-academy/fancy-flow/terminal/fancy-term-host\";\n *\n * useFancyTermHost(terminalManager());\n * ```\n *\n * `PtyBackend` satisfies `PtyBackendLike` structurally — verified against\n * `fancy-term-host@0.5.0`, `src/backend.ts`. The narrow shape below is the\n * whole of what this adapter touches, which is what keeps that claim small\n * enough to re-check.\n */\n\nimport { registerTerminalHost, type TerminalHost, type TerminalSession, type TerminalSessionSpec } from \"../registry/capabilities\";\n\n/** The slice of `fancy-term-host`'s `PtyBackend` this adapter uses. */\nexport type PtyBackendLike = {\n create(opts: {\n id: string;\n cwd: string;\n shell?: string;\n args?: string[];\n cols?: number;\n rows?: number;\n env?: Record<string, string>;\n }): { id: string; pid: number; shell: string };\n write(id: string, data: string): boolean;\n kill(id: string): boolean;\n on(event: \"data\", listener: (id: string, data: string) => void): unknown;\n on(event: \"exit\", listener: (id: string, payload: { exitCode: number; signal?: number }) => void): unknown;\n};\n\n/** Applied when a lane does not declare its own. */\nexport type FancyTermHostDefaults = {\n /** Where a lane's shell starts. `fancy-term-host` requires one; this supplies it. */\n cwd?: string;\n /** Extra args for the shell a lane names. */\n args?: string[];\n /** Merged UNDER the lane's own env, so a lane can override a default. */\n env?: Record<string, string>;\n cols?: number;\n rows?: number;\n /** Ids for new sessions. Override in a test that wants them predictable. */\n newId?: () => string;\n};\n\n/**\n * One fan-out per backend.\n *\n * `PtyBackend` exposes `on` and no `off`, so a listener added per session could\n * never be removed — a long run would accumulate one dead listener per terminal\n * it opened, and Node would eventually warn about a leak that is really ours.\n *\n * So each backend is subscribed to exactly ONCE and the per-session listeners\n * live in a map this module controls, where unsubscribing is just a delete.\n * Keyed weakly, so a backend that goes out of scope is not retained by us.\n */\ntype Fanout = {\n data: Map<string, Set<(chunk: string) => void>>;\n exit: Map<string, (exit: { exitCode: number; signal?: string }) => void>;\n};\n\nconst fanouts = new WeakMap<PtyBackendLike, Fanout>();\n\nfunction fanoutFor(backend: PtyBackendLike): Fanout {\n const existing = fanouts.get(backend);\n if (existing) return existing;\n\n const fanout: Fanout = { data: new Map(), exit: new Map() };\n fanouts.set(backend, fanout);\n\n backend.on(\"data\", (id, chunk) => {\n const listeners = fanout.data.get(id);\n if (!listeners) return;\n // Copied before iterating: a listener that unsubscribes itself while being\n // notified would otherwise mutate the set mid-iteration.\n for (const listener of [...listeners]) listener(chunk);\n });\n\n backend.on(\"exit\", (id, payload) => {\n fanout.exit.get(id)?.({\n exitCode: payload.exitCode,\n // fancy-term-host reports a signal NUMBER; `TerminalExit.signal` is a\n // string, because a signal is a name everywhere a person reads one.\n signal: payload.signal === undefined ? undefined : String(payload.signal),\n });\n fanout.exit.delete(id);\n fanout.data.delete(id);\n });\n\n return fanout;\n}\n\n/**\n * The process's working directory, without depending on `@types/node`.\n *\n * fancy-flow builds for the browser too and carries no Node types, so `process`\n * is not a name this package can reference directly. Reached through\n * `globalThis` it stays honest in both places: a real answer where there is a\n * process, and `\".\"` where there is not, rather than a build that only compiles\n * in one of them.\n */\nfunction runtimeCwd(): string {\n const proc = (globalThis as { process?: { cwd?: () => string } }).process;\n return typeof proc?.cwd === \"function\" ? proc.cwd() : \".\";\n}\n\nlet counter = 0;\n\nfunction defaultNewId(): string {\n counter += 1;\n return `flow-${Date.now().toString(36)}-${counter.toString(36)}`;\n}\n\n/** Build a `TerminalHost` over a `fancy-term-host` backend. */\nexport function createFancyTermHost(\n backend: PtyBackendLike,\n defaults: FancyTermHostDefaults = {},\n): TerminalHost {\n const fanout = fanoutFor(backend);\n const newId = defaults.newId ?? defaultNewId;\n\n return {\n open: (spec: TerminalSessionSpec): TerminalSession => {\n const id = newId();\n\n // Registered BEFORE `create`, not after. A shell that dies immediately —\n // a bad `cwd`, a shell that is not installed — emits `exit` during or\n // right after the spawn, and a listener attached afterwards would miss\n // it. The run would then wait out its full timeout and report \"the\n // command did not finish\" about a process that never started.\n let settleExit: (exit: { exitCode: number; signal?: string }) => void = () => {};\n const exited = new Promise<{ exitCode: number; signal?: string }>((resolve) => {\n settleExit = resolve;\n });\n fanout.exit.set(id, settleExit);\n\n const listeners = new Set<(chunk: string) => void>();\n fanout.data.set(id, listeners);\n\n const info = backend.create({\n id,\n // Required by `CreateTerminalOpts`, optional in a lane's config —\n // `process.cwd()` is the same default a person typing in a terminal\n // would get.\n cwd: spec.cwd ?? defaults.cwd ?? runtimeCwd(),\n shell: spec.command ?? undefined,\n args: spec.args ?? defaults.args,\n cols: spec.cols ?? defaults.cols,\n rows: spec.rows ?? defaults.rows,\n // The lane's own env wins: a default exists to fill a gap, not to\n // override something an author wrote on the node.\n env: { ...defaults.env, ...spec.env },\n });\n\n return {\n id: info.id,\n write: (data) => { backend.write(id, data); },\n onData: (listener) => {\n listeners.add(listener);\n return () => { listeners.delete(listener); };\n },\n exited,\n close: () => {\n backend.kill(id);\n fanout.data.delete(id);\n // Resolve rather than leave hanging. `exited` is raced by every wait,\n // so an unresolved promise after a deliberate close would keep a\n // pending timer alive for nothing.\n settleExit({ exitCode: 0 });\n fanout.exit.delete(id);\n },\n };\n },\n };\n}\n\n/**\n * Install it. Returns the unregister function, exactly as\n * `registerTerminalHost` does.\n */\nexport function useFancyTermHost(\n backend: PtyBackendLike,\n defaults: FancyTermHostDefaults = {},\n): () => void {\n return registerTerminalHost(createFancyTermHost(backend, defaults));\n}\n"]}
@@ -0,0 +1,76 @@
1
+ import { T as TerminalHost } from '../types-B-Syk9-M.cjs';
2
+ import '@xyflow/react';
3
+
4
+ /**
5
+ * A `TerminalHost` backed by `@particle-academy/fancy-term-host`.
6
+ *
7
+ * ## Why this takes the backend as an argument instead of importing it
8
+ *
9
+ * `fancy-term-host` declares `node-pty` as a REQUIRED peer, and its `manager.ts`
10
+ * imports it at module scope — so importing the package at all pulls a native
11
+ * addon in at load time. Depending on it here, even as a devDependency for the
12
+ * types, would put a native build in fancy-flow's own CI install on every
13
+ * platform, for a package that does not otherwise need one.
14
+ *
15
+ * The consumer who wants this adapter is a desktop app that has already wired
16
+ * `fancy-term-host` and holds a live backend. Handing it in costs them one
17
+ * argument and costs everyone else nothing:
18
+ *
19
+ * ```ts
20
+ * import { terminalManager } from "@particle-academy/fancy-term-host";
21
+ * import { useFancyTermHost } from "@particle-academy/fancy-flow/terminal/fancy-term-host";
22
+ *
23
+ * useFancyTermHost(terminalManager());
24
+ * ```
25
+ *
26
+ * `PtyBackend` satisfies `PtyBackendLike` structurally — verified against
27
+ * `fancy-term-host@0.5.0`, `src/backend.ts`. The narrow shape below is the
28
+ * whole of what this adapter touches, which is what keeps that claim small
29
+ * enough to re-check.
30
+ */
31
+
32
+ /** The slice of `fancy-term-host`'s `PtyBackend` this adapter uses. */
33
+ type PtyBackendLike = {
34
+ create(opts: {
35
+ id: string;
36
+ cwd: string;
37
+ shell?: string;
38
+ args?: string[];
39
+ cols?: number;
40
+ rows?: number;
41
+ env?: Record<string, string>;
42
+ }): {
43
+ id: string;
44
+ pid: number;
45
+ shell: string;
46
+ };
47
+ write(id: string, data: string): boolean;
48
+ kill(id: string): boolean;
49
+ on(event: "data", listener: (id: string, data: string) => void): unknown;
50
+ on(event: "exit", listener: (id: string, payload: {
51
+ exitCode: number;
52
+ signal?: number;
53
+ }) => void): unknown;
54
+ };
55
+ /** Applied when a lane does not declare its own. */
56
+ type FancyTermHostDefaults = {
57
+ /** Where a lane's shell starts. `fancy-term-host` requires one; this supplies it. */
58
+ cwd?: string;
59
+ /** Extra args for the shell a lane names. */
60
+ args?: string[];
61
+ /** Merged UNDER the lane's own env, so a lane can override a default. */
62
+ env?: Record<string, string>;
63
+ cols?: number;
64
+ rows?: number;
65
+ /** Ids for new sessions. Override in a test that wants them predictable. */
66
+ newId?: () => string;
67
+ };
68
+ /** Build a `TerminalHost` over a `fancy-term-host` backend. */
69
+ declare function createFancyTermHost(backend: PtyBackendLike, defaults?: FancyTermHostDefaults): TerminalHost;
70
+ /**
71
+ * Install it. Returns the unregister function, exactly as
72
+ * `registerTerminalHost` does.
73
+ */
74
+ declare function useFancyTermHost(backend: PtyBackendLike, defaults?: FancyTermHostDefaults): () => void;
75
+
76
+ export { type FancyTermHostDefaults, type PtyBackendLike, createFancyTermHost, useFancyTermHost };
@@ -0,0 +1,76 @@
1
+ import { T as TerminalHost } from '../types-B-Syk9-M.js';
2
+ import '@xyflow/react';
3
+
4
+ /**
5
+ * A `TerminalHost` backed by `@particle-academy/fancy-term-host`.
6
+ *
7
+ * ## Why this takes the backend as an argument instead of importing it
8
+ *
9
+ * `fancy-term-host` declares `node-pty` as a REQUIRED peer, and its `manager.ts`
10
+ * imports it at module scope — so importing the package at all pulls a native
11
+ * addon in at load time. Depending on it here, even as a devDependency for the
12
+ * types, would put a native build in fancy-flow's own CI install on every
13
+ * platform, for a package that does not otherwise need one.
14
+ *
15
+ * The consumer who wants this adapter is a desktop app that has already wired
16
+ * `fancy-term-host` and holds a live backend. Handing it in costs them one
17
+ * argument and costs everyone else nothing:
18
+ *
19
+ * ```ts
20
+ * import { terminalManager } from "@particle-academy/fancy-term-host";
21
+ * import { useFancyTermHost } from "@particle-academy/fancy-flow/terminal/fancy-term-host";
22
+ *
23
+ * useFancyTermHost(terminalManager());
24
+ * ```
25
+ *
26
+ * `PtyBackend` satisfies `PtyBackendLike` structurally — verified against
27
+ * `fancy-term-host@0.5.0`, `src/backend.ts`. The narrow shape below is the
28
+ * whole of what this adapter touches, which is what keeps that claim small
29
+ * enough to re-check.
30
+ */
31
+
32
+ /** The slice of `fancy-term-host`'s `PtyBackend` this adapter uses. */
33
+ type PtyBackendLike = {
34
+ create(opts: {
35
+ id: string;
36
+ cwd: string;
37
+ shell?: string;
38
+ args?: string[];
39
+ cols?: number;
40
+ rows?: number;
41
+ env?: Record<string, string>;
42
+ }): {
43
+ id: string;
44
+ pid: number;
45
+ shell: string;
46
+ };
47
+ write(id: string, data: string): boolean;
48
+ kill(id: string): boolean;
49
+ on(event: "data", listener: (id: string, data: string) => void): unknown;
50
+ on(event: "exit", listener: (id: string, payload: {
51
+ exitCode: number;
52
+ signal?: number;
53
+ }) => void): unknown;
54
+ };
55
+ /** Applied when a lane does not declare its own. */
56
+ type FancyTermHostDefaults = {
57
+ /** Where a lane's shell starts. `fancy-term-host` requires one; this supplies it. */
58
+ cwd?: string;
59
+ /** Extra args for the shell a lane names. */
60
+ args?: string[];
61
+ /** Merged UNDER the lane's own env, so a lane can override a default. */
62
+ env?: Record<string, string>;
63
+ cols?: number;
64
+ rows?: number;
65
+ /** Ids for new sessions. Override in a test that wants them predictable. */
66
+ newId?: () => string;
67
+ };
68
+ /** Build a `TerminalHost` over a `fancy-term-host` backend. */
69
+ declare function createFancyTermHost(backend: PtyBackendLike, defaults?: FancyTermHostDefaults): TerminalHost;
70
+ /**
71
+ * Install it. Returns the unregister function, exactly as
72
+ * `registerTerminalHost` does.
73
+ */
74
+ declare function useFancyTermHost(backend: PtyBackendLike, defaults?: FancyTermHostDefaults): () => void;
75
+
76
+ export { type FancyTermHostDefaults, type PtyBackendLike, createFancyTermHost, useFancyTermHost };
@@ -0,0 +1,92 @@
1
+ import { registerTerminalHost } from '../chunk-2QXKDLGT.js';
2
+
3
+ // src/terminal/fancy-term-host.ts
4
+ var fanouts = /* @__PURE__ */ new WeakMap();
5
+ function fanoutFor(backend) {
6
+ const existing = fanouts.get(backend);
7
+ if (existing) return existing;
8
+ const fanout = { data: /* @__PURE__ */ new Map(), exit: /* @__PURE__ */ new Map() };
9
+ fanouts.set(backend, fanout);
10
+ backend.on("data", (id, chunk) => {
11
+ const listeners = fanout.data.get(id);
12
+ if (!listeners) return;
13
+ for (const listener of [...listeners]) listener(chunk);
14
+ });
15
+ backend.on("exit", (id, payload) => {
16
+ fanout.exit.get(id)?.({
17
+ exitCode: payload.exitCode,
18
+ // fancy-term-host reports a signal NUMBER; `TerminalExit.signal` is a
19
+ // string, because a signal is a name everywhere a person reads one.
20
+ signal: payload.signal === void 0 ? void 0 : String(payload.signal)
21
+ });
22
+ fanout.exit.delete(id);
23
+ fanout.data.delete(id);
24
+ });
25
+ return fanout;
26
+ }
27
+ function runtimeCwd() {
28
+ const proc = globalThis.process;
29
+ return typeof proc?.cwd === "function" ? proc.cwd() : ".";
30
+ }
31
+ var counter = 0;
32
+ function defaultNewId() {
33
+ counter += 1;
34
+ return `flow-${Date.now().toString(36)}-${counter.toString(36)}`;
35
+ }
36
+ function createFancyTermHost(backend, defaults = {}) {
37
+ const fanout = fanoutFor(backend);
38
+ const newId = defaults.newId ?? defaultNewId;
39
+ return {
40
+ open: (spec) => {
41
+ const id = newId();
42
+ let settleExit = () => {
43
+ };
44
+ const exited = new Promise((resolve) => {
45
+ settleExit = resolve;
46
+ });
47
+ fanout.exit.set(id, settleExit);
48
+ const listeners = /* @__PURE__ */ new Set();
49
+ fanout.data.set(id, listeners);
50
+ const info = backend.create({
51
+ id,
52
+ // Required by `CreateTerminalOpts`, optional in a lane's config —
53
+ // `process.cwd()` is the same default a person typing in a terminal
54
+ // would get.
55
+ cwd: spec.cwd ?? defaults.cwd ?? runtimeCwd(),
56
+ shell: spec.command ?? void 0,
57
+ args: spec.args ?? defaults.args,
58
+ cols: spec.cols ?? defaults.cols,
59
+ rows: spec.rows ?? defaults.rows,
60
+ // The lane's own env wins: a default exists to fill a gap, not to
61
+ // override something an author wrote on the node.
62
+ env: { ...defaults.env, ...spec.env }
63
+ });
64
+ return {
65
+ id: info.id,
66
+ write: (data) => {
67
+ backend.write(id, data);
68
+ },
69
+ onData: (listener) => {
70
+ listeners.add(listener);
71
+ return () => {
72
+ listeners.delete(listener);
73
+ };
74
+ },
75
+ exited,
76
+ close: () => {
77
+ backend.kill(id);
78
+ fanout.data.delete(id);
79
+ settleExit({ exitCode: 0 });
80
+ fanout.exit.delete(id);
81
+ }
82
+ };
83
+ }
84
+ };
85
+ }
86
+ function useFancyTermHost(backend, defaults = {}) {
87
+ return registerTerminalHost(createFancyTermHost(backend, defaults));
88
+ }
89
+
90
+ export { createFancyTermHost, useFancyTermHost };
91
+ //# sourceMappingURL=fancy-term-host.js.map
92
+ //# sourceMappingURL=fancy-term-host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/terminal/fancy-term-host.ts"],"names":[],"mappings":"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","file":"fancy-term-host.js","sourcesContent":["/**\n * A `TerminalHost` backed by `@particle-academy/fancy-term-host`.\n *\n * ## Why this takes the backend as an argument instead of importing it\n *\n * `fancy-term-host` declares `node-pty` as a REQUIRED peer, and its `manager.ts`\n * imports it at module scope — so importing the package at all pulls a native\n * addon in at load time. Depending on it here, even as a devDependency for the\n * types, would put a native build in fancy-flow's own CI install on every\n * platform, for a package that does not otherwise need one.\n *\n * The consumer who wants this adapter is a desktop app that has already wired\n * `fancy-term-host` and holds a live backend. Handing it in costs them one\n * argument and costs everyone else nothing:\n *\n * ```ts\n * import { terminalManager } from \"@particle-academy/fancy-term-host\";\n * import { useFancyTermHost } from \"@particle-academy/fancy-flow/terminal/fancy-term-host\";\n *\n * useFancyTermHost(terminalManager());\n * ```\n *\n * `PtyBackend` satisfies `PtyBackendLike` structurally — verified against\n * `fancy-term-host@0.5.0`, `src/backend.ts`. The narrow shape below is the\n * whole of what this adapter touches, which is what keeps that claim small\n * enough to re-check.\n */\n\nimport { registerTerminalHost, type TerminalHost, type TerminalSession, type TerminalSessionSpec } from \"../registry/capabilities\";\n\n/** The slice of `fancy-term-host`'s `PtyBackend` this adapter uses. */\nexport type PtyBackendLike = {\n create(opts: {\n id: string;\n cwd: string;\n shell?: string;\n args?: string[];\n cols?: number;\n rows?: number;\n env?: Record<string, string>;\n }): { id: string; pid: number; shell: string };\n write(id: string, data: string): boolean;\n kill(id: string): boolean;\n on(event: \"data\", listener: (id: string, data: string) => void): unknown;\n on(event: \"exit\", listener: (id: string, payload: { exitCode: number; signal?: number }) => void): unknown;\n};\n\n/** Applied when a lane does not declare its own. */\nexport type FancyTermHostDefaults = {\n /** Where a lane's shell starts. `fancy-term-host` requires one; this supplies it. */\n cwd?: string;\n /** Extra args for the shell a lane names. */\n args?: string[];\n /** Merged UNDER the lane's own env, so a lane can override a default. */\n env?: Record<string, string>;\n cols?: number;\n rows?: number;\n /** Ids for new sessions. Override in a test that wants them predictable. */\n newId?: () => string;\n};\n\n/**\n * One fan-out per backend.\n *\n * `PtyBackend` exposes `on` and no `off`, so a listener added per session could\n * never be removed — a long run would accumulate one dead listener per terminal\n * it opened, and Node would eventually warn about a leak that is really ours.\n *\n * So each backend is subscribed to exactly ONCE and the per-session listeners\n * live in a map this module controls, where unsubscribing is just a delete.\n * Keyed weakly, so a backend that goes out of scope is not retained by us.\n */\ntype Fanout = {\n data: Map<string, Set<(chunk: string) => void>>;\n exit: Map<string, (exit: { exitCode: number; signal?: string }) => void>;\n};\n\nconst fanouts = new WeakMap<PtyBackendLike, Fanout>();\n\nfunction fanoutFor(backend: PtyBackendLike): Fanout {\n const existing = fanouts.get(backend);\n if (existing) return existing;\n\n const fanout: Fanout = { data: new Map(), exit: new Map() };\n fanouts.set(backend, fanout);\n\n backend.on(\"data\", (id, chunk) => {\n const listeners = fanout.data.get(id);\n if (!listeners) return;\n // Copied before iterating: a listener that unsubscribes itself while being\n // notified would otherwise mutate the set mid-iteration.\n for (const listener of [...listeners]) listener(chunk);\n });\n\n backend.on(\"exit\", (id, payload) => {\n fanout.exit.get(id)?.({\n exitCode: payload.exitCode,\n // fancy-term-host reports a signal NUMBER; `TerminalExit.signal` is a\n // string, because a signal is a name everywhere a person reads one.\n signal: payload.signal === undefined ? undefined : String(payload.signal),\n });\n fanout.exit.delete(id);\n fanout.data.delete(id);\n });\n\n return fanout;\n}\n\n/**\n * The process's working directory, without depending on `@types/node`.\n *\n * fancy-flow builds for the browser too and carries no Node types, so `process`\n * is not a name this package can reference directly. Reached through\n * `globalThis` it stays honest in both places: a real answer where there is a\n * process, and `\".\"` where there is not, rather than a build that only compiles\n * in one of them.\n */\nfunction runtimeCwd(): string {\n const proc = (globalThis as { process?: { cwd?: () => string } }).process;\n return typeof proc?.cwd === \"function\" ? proc.cwd() : \".\";\n}\n\nlet counter = 0;\n\nfunction defaultNewId(): string {\n counter += 1;\n return `flow-${Date.now().toString(36)}-${counter.toString(36)}`;\n}\n\n/** Build a `TerminalHost` over a `fancy-term-host` backend. */\nexport function createFancyTermHost(\n backend: PtyBackendLike,\n defaults: FancyTermHostDefaults = {},\n): TerminalHost {\n const fanout = fanoutFor(backend);\n const newId = defaults.newId ?? defaultNewId;\n\n return {\n open: (spec: TerminalSessionSpec): TerminalSession => {\n const id = newId();\n\n // Registered BEFORE `create`, not after. A shell that dies immediately —\n // a bad `cwd`, a shell that is not installed — emits `exit` during or\n // right after the spawn, and a listener attached afterwards would miss\n // it. The run would then wait out its full timeout and report \"the\n // command did not finish\" about a process that never started.\n let settleExit: (exit: { exitCode: number; signal?: string }) => void = () => {};\n const exited = new Promise<{ exitCode: number; signal?: string }>((resolve) => {\n settleExit = resolve;\n });\n fanout.exit.set(id, settleExit);\n\n const listeners = new Set<(chunk: string) => void>();\n fanout.data.set(id, listeners);\n\n const info = backend.create({\n id,\n // Required by `CreateTerminalOpts`, optional in a lane's config —\n // `process.cwd()` is the same default a person typing in a terminal\n // would get.\n cwd: spec.cwd ?? defaults.cwd ?? runtimeCwd(),\n shell: spec.command ?? undefined,\n args: spec.args ?? defaults.args,\n cols: spec.cols ?? defaults.cols,\n rows: spec.rows ?? defaults.rows,\n // The lane's own env wins: a default exists to fill a gap, not to\n // override something an author wrote on the node.\n env: { ...defaults.env, ...spec.env },\n });\n\n return {\n id: info.id,\n write: (data) => { backend.write(id, data); },\n onData: (listener) => {\n listeners.add(listener);\n return () => { listeners.delete(listener); };\n },\n exited,\n close: () => {\n backend.kill(id);\n fanout.data.delete(id);\n // Resolve rather than leave hanging. `exited` is raced by every wait,\n // so an unresolved promise after a deliberate close would keep a\n // pending timer alive for nothing.\n settleExit({ exitCode: 0 });\n fanout.exit.delete(id);\n },\n };\n },\n };\n}\n\n/**\n * Install it. Returns the unregister function, exactly as\n * `registerTerminalHost` does.\n */\nexport function useFancyTermHost(\n backend: PtyBackendLike,\n defaults: FancyTermHostDefaults = {},\n): () => void {\n return registerTerminalHost(createFancyTermHost(backend, defaults));\n}\n"]}