@synergenius/flow-weaver 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,12 @@
1
1
  # @synergenius/flow-weaver
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/@synergenius/flow-weaver.svg)](https://www.npmjs.com/package/@synergenius/flow-weaver)
4
- [![License: Flow Weaver Library License](https://img.shields.io/badge/License-Flow%20Weaver%20Library-blue.svg)](./LICENSE)
5
- [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-green.svg)](https://nodejs.org)
3
+ [![npm version](https://img.shields.io/npm/v/@synergenius/flow-weaver?style=flat)](https://www.npmjs.com/package/@synergenius/flow-weaver)
4
+ [![npm downloads](https://img.shields.io/npm/dw/@synergenius/flow-weaver?style=flat)](https://www.npmjs.com/package/@synergenius/flow-weaver)
5
+ [![CI](https://img.shields.io/github/actions/workflow/status/synergenius-fw/flow-weaver/ci.yml?branch=main&style=flat)](https://github.com/synergenius-fw/flow-weaver/actions/workflows/ci.yml)
6
+ [![Tests](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/moraispgsi/305430ef59a51d0a58eb61fefdfbe634/raw/flow-weaver-test-count.json&style=flat)](https://github.com/synergenius-fw/flow-weaver/actions/workflows/ci.yml)
7
+ [![Coverage](https://img.shields.io/codecov/c/github/synergenius-fw/flow-weaver?style=flat)](https://codecov.io/gh/synergenius-fw/flow-weaver)
8
+ [![License: Flow Weaver Library License](https://img.shields.io/badge/License-Flow%20Weaver%20Library-blue?style=flat)](./LICENSE)
9
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-green?style=flat)](https://nodejs.org)
6
10
 
7
11
  **Build AI agent workflows visually. Ship them as your own code.**
8
12
 
@@ -10,11 +14,17 @@ Design agent workflows in the Studio, in TypeScript, or let AI build them for yo
10
14
 
11
15
  ## Three Ways to Build
12
16
 
13
- **Studio.** Drag, drop, connect. The visual editor renders your workflow as an interactive graph with bidirectional sync: canvas changes write code, code changes update the canvas. 80+ plugins handle rendering, state, minimap, undo/redo, and more.
17
+ **Studio.**
18
+ A unified environment that combines a full code editor with a visual graph builder. You can write and refactor workflows directly in code or compose them visually on the canvas. Both representations stay synchronized at all times. Editing either one updates the other instantly, so you can move between abstraction levels without friction.
14
19
 
15
- **TypeScript.** Annotate plain functions with JSDoc tags. The compiler turns them into executable workflow graphs with full type safety, IDE autocomplete, and compile-time validation. No YAML, no JSON configs.
20
+ **TypeScript.**
21
+ Define workflows in plain TypeScript by annotating functions with JSDoc. The compiler derives an executable workflow graph with static typing and compile-time validation.
22
+ There is no YAML, no JSON configuration, and no runtime layer.
23
+ No lock-in. Remove the annotations and you keep a clean, readable TypeScript file with zero dependencies.
16
24
 
17
- **AI Agents.** Connect Claude Code, Cursor, or OpenClaw and they scaffold, validate, and ship workflows using 30+ MCP tools. The agent reads validation errors, fixes issues, and re-validates until the workflow compiles clean. The development loop (steps 1-4 are fully autonomous):
25
+ **AI Agents.**
26
+ Connect Claude Code, Cursor, or OpenClaw to design and ship workflows. Agents scaffold implementations, run the compiler, interpret validation errors, apply corrections, and repeat the process until the workflow compiles successfully.
27
+ With more than 30 MCP tools available, the entire build loop can run autonomously.
18
28
 
19
29
  1. **Agent creates**: scaffolds from templates, builds from a model, or writes from scratch
20
30
  2. **Compiler validates**: 15+ validation passes catch missing connections, type mismatches, unreachable paths
@@ -16881,6 +16881,121 @@ var require_browser = __commonJS({
16881
16881
  }
16882
16882
  });
16883
16883
 
16884
+ // node_modules/has-flag/index.js
16885
+ var require_has_flag = __commonJS({
16886
+ "node_modules/has-flag/index.js"(exports2, module2) {
16887
+ "use strict";
16888
+ module2.exports = (flag, argv = process.argv) => {
16889
+ const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
16890
+ const position = argv.indexOf(prefix + flag);
16891
+ const terminatorPosition = argv.indexOf("--");
16892
+ return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
16893
+ };
16894
+ }
16895
+ });
16896
+
16897
+ // node_modules/supports-color/index.js
16898
+ var require_supports_color = __commonJS({
16899
+ "node_modules/supports-color/index.js"(exports2, module2) {
16900
+ "use strict";
16901
+ var os3 = __require("os");
16902
+ var tty = __require("tty");
16903
+ var hasFlag = require_has_flag();
16904
+ var { env } = process;
16905
+ var forceColor;
16906
+ if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) {
16907
+ forceColor = 0;
16908
+ } else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) {
16909
+ forceColor = 1;
16910
+ }
16911
+ if ("FORCE_COLOR" in env) {
16912
+ if (env.FORCE_COLOR === "true") {
16913
+ forceColor = 1;
16914
+ } else if (env.FORCE_COLOR === "false") {
16915
+ forceColor = 0;
16916
+ } else {
16917
+ forceColor = env.FORCE_COLOR.length === 0 ? 1 : Math.min(parseInt(env.FORCE_COLOR, 10), 3);
16918
+ }
16919
+ }
16920
+ function translateLevel(level) {
16921
+ if (level === 0) {
16922
+ return false;
16923
+ }
16924
+ return {
16925
+ level,
16926
+ hasBasic: true,
16927
+ has256: level >= 2,
16928
+ has16m: level >= 3
16929
+ };
16930
+ }
16931
+ function supportsColor(haveStream, streamIsTTY) {
16932
+ if (forceColor === 0) {
16933
+ return 0;
16934
+ }
16935
+ if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
16936
+ return 3;
16937
+ }
16938
+ if (hasFlag("color=256")) {
16939
+ return 2;
16940
+ }
16941
+ if (haveStream && !streamIsTTY && forceColor === void 0) {
16942
+ return 0;
16943
+ }
16944
+ const min = forceColor || 0;
16945
+ if (env.TERM === "dumb") {
16946
+ return min;
16947
+ }
16948
+ if (process.platform === "win32") {
16949
+ const osRelease = os3.release().split(".");
16950
+ if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
16951
+ return Number(osRelease[2]) >= 14931 ? 3 : 2;
16952
+ }
16953
+ return 1;
16954
+ }
16955
+ if ("CI" in env) {
16956
+ if (["TRAVIS", "CIRCLECI", "APPVEYOR", "GITLAB_CI", "GITHUB_ACTIONS", "BUILDKITE"].some((sign) => sign in env) || env.CI_NAME === "codeship") {
16957
+ return 1;
16958
+ }
16959
+ return min;
16960
+ }
16961
+ if ("TEAMCITY_VERSION" in env) {
16962
+ return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
16963
+ }
16964
+ if (env.COLORTERM === "truecolor") {
16965
+ return 3;
16966
+ }
16967
+ if ("TERM_PROGRAM" in env) {
16968
+ const version3 = parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
16969
+ switch (env.TERM_PROGRAM) {
16970
+ case "iTerm.app":
16971
+ return version3 >= 3 ? 3 : 2;
16972
+ case "Apple_Terminal":
16973
+ return 2;
16974
+ }
16975
+ }
16976
+ if (/-256(color)?$/i.test(env.TERM)) {
16977
+ return 2;
16978
+ }
16979
+ if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
16980
+ return 1;
16981
+ }
16982
+ if ("COLORTERM" in env) {
16983
+ return 1;
16984
+ }
16985
+ return min;
16986
+ }
16987
+ function getSupportLevel(stream2) {
16988
+ const level = supportsColor(stream2, stream2 && stream2.isTTY);
16989
+ return translateLevel(level);
16990
+ }
16991
+ module2.exports = {
16992
+ supportsColor: getSupportLevel,
16993
+ stdout: translateLevel(supportsColor(true, tty.isatty(1))),
16994
+ stderr: translateLevel(supportsColor(true, tty.isatty(2)))
16995
+ };
16996
+ }
16997
+ });
16998
+
16884
16999
  // node_modules/debug/src/node.js
16885
17000
  var require_node = __commonJS({
16886
17001
  "node_modules/debug/src/node.js"(exports2, module2) {
@@ -16899,7 +17014,7 @@ var require_node = __commonJS({
16899
17014
  );
16900
17015
  exports2.colors = [6, 2, 3, 4, 5, 1];
16901
17016
  try {
16902
- const supportsColor = __require("supports-color");
17017
+ const supportsColor = require_supports_color();
16903
17018
  if (supportsColor && (supportsColor.stderr || supportsColor).level >= 2) {
16904
17019
  exports2.colors = [
16905
17020
  20,
@@ -61065,12 +61180,12 @@ function renderConnection(parts2, conn, gradIndex) {
61065
61180
  ` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="3"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}:output" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}:input"/>`
61066
61181
  );
61067
61182
  }
61068
- function renderScopeConnection(parts2, conn, allConnections) {
61183
+ function renderScopeConnection(parts2, conn, allConnections, parentNodeId) {
61069
61184
  const gradIndex = allConnections.indexOf(conn);
61070
61185
  if (gradIndex < 0) return;
61071
61186
  const dashAttr = conn.isStepConnection ? "" : ' stroke-dasharray="8 4"';
61072
61187
  parts2.push(
61073
- ` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="2.5"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}:output" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}:input"/>`
61188
+ ` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="2.5"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}:output" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}:input" data-scope="${escapeXml(parentNodeId)}"/>`
61074
61189
  );
61075
61190
  }
61076
61191
  function renderNodeBody(parts2, node, theme, indent) {
@@ -61114,7 +61229,7 @@ function renderScopedContent(parts2, node, theme, themeName, allConnections) {
61114
61229
  ` <rect x="${scopeX}" y="${scopeY}" width="${scopeW}" height="${scopeH}" rx="4" fill="none" stroke="${theme.scopeAreaStroke}" stroke-width="1" stroke-dasharray="4 2" opacity="0.5"/>`
61115
61230
  );
61116
61231
  for (const conn of node.scopeConnections ?? []) {
61117
- renderScopeConnection(parts2, conn, allConnections);
61232
+ renderScopeConnection(parts2, conn, allConnections, node.id);
61118
61233
  }
61119
61234
  if (scopePorts) {
61120
61235
  renderPortDots(parts2, node.id, scopePorts.inputs, scopePorts.outputs, themeName);
@@ -61351,6 +61466,21 @@ path[data-source].port-hover { opacity: 1; }
61351
61466
  border-radius: 3px; font-family: 'SF Mono', 'Fira Code', monospace;
61352
61467
  font-size: 12px; background: rgba(255,255,255,0.1);
61353
61468
  }
61469
+
61470
+ /* Studio nudge toast */
61471
+ #studio-hint {
61472
+ position: fixed; bottom: 60px; left: 50%;
61473
+ transform: translateX(-50%);
61474
+ background: ${surfaceMain}; border: 1px solid ${borderSubtle};
61475
+ padding: 8px 16px; border-radius: 8px;
61476
+ font-size: 13px; color: ${textMed};
61477
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
61478
+ z-index: 20; opacity: 0; transition: opacity 0.4s;
61479
+ pointer-events: none;
61480
+ }
61481
+ #studio-hint.visible { opacity: 1; pointer-events: auto; }
61482
+ #studio-hint a { color: ${brandAccent}; text-decoration: none; font-weight: 600; }
61483
+ #studio-hint a:hover { text-decoration: underline; }
61354
61484
  </style>
61355
61485
  </head>
61356
61486
  <body>
@@ -61374,6 +61504,12 @@ path[data-source].port-hover { opacity: 1; }
61374
61504
  <path d="M1 5h12M1 9h12M5 1v12M9 1v12" opacity="0.4"/>
61375
61505
  </svg>
61376
61506
  </button>
61507
+ <button class="ctrl-btn" id="btn-reset" title="Reset layout" aria-label="Reset layout">
61508
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5">
61509
+ <path d="M1.5 2v3.5h3.5"/>
61510
+ <path d="M2.1 8.5a5 5 0 1 0 .9-4L1.5 5.5"/>
61511
+ </svg>
61512
+ </button>
61377
61513
  </div>
61378
61514
  <div id="info-panel">
61379
61515
  <h3 id="info-title"></h3>
@@ -61384,6 +61520,7 @@ path[data-source].port-hover { opacity: 1; }
61384
61520
  <span>Flow Weaver</span>
61385
61521
  </a>
61386
61522
  <div id="scroll-hint">Use <kbd id="mod-key">Ctrl</kbd> + scroll to zoom</div>
61523
+ <div id="studio-hint">Like rearranging? <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a> saves your layouts.</div>
61387
61524
  <script>
61388
61525
  (function() {
61389
61526
  'use strict';
@@ -61396,6 +61533,7 @@ path[data-source].port-hover { opacity: 1; }
61396
61533
  var infoTitle = document.getElementById('info-title');
61397
61534
  var infoBody = document.getElementById('info-body');
61398
61535
  var scrollHint = document.getElementById('scroll-hint');
61536
+ var studioHint = document.getElementById('studio-hint');
61399
61537
 
61400
61538
  // Parse the original viewBox (diagram bounding box)
61401
61539
  var vbParts = '${viewBox}'.split(/\\s+/).map(Number);
@@ -61467,6 +61605,17 @@ path[data-source].port-hover { opacity: 1; }
61467
61605
 
61468
61606
  // ---- Pan (drag) + Node drag ----
61469
61607
  var draggedNodeId = null, dragNodeStart = null, didDragNode = false;
61608
+ var dragCount = 0, nudgeIndex = 0, nudgeTimer = null;
61609
+ var nudgeMessages = [
61610
+ 'Like rearranging? <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a> saves your layouts.',
61611
+ 'Changes here are temporary. <a href="https://flowweaver.ai" target="_blank" rel="noopener">Try the Studio</a> to keep them.',
61612
+ 'Want to collaborate? <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a> has real-time sharing.',
61613
+ 'Build and deploy from the cloud with <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a>.',
61614
+ 'This viewer is read-only. <a href="https://flowweaver.ai" target="_blank" rel="noopener">Build workflows</a> in the Studio.',
61615
+ 'Version history, diff viewer, rollbacks. All in <a href="https://flowweaver.ai" target="_blank" rel="noopener">the Studio</a>.',
61616
+ 'Debug workflows step by step in <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a>.',
61617
+ 'Ship faster. <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a> runs your workflows in the cloud.'
61618
+ ];
61470
61619
 
61471
61620
  canvas.addEventListener('pointerdown', function(e) {
61472
61621
  if (e.button !== 0) return;
@@ -61525,6 +61674,18 @@ path[data-source].port-hover { opacity: 1; }
61525
61674
  });
61526
61675
 
61527
61676
  function endDrag() {
61677
+ if (didDragNode) {
61678
+ dragCount++;
61679
+ var threshold = nudgeIndex === 0 ? 3 : 5;
61680
+ if (dragCount >= threshold) {
61681
+ dragCount = 0;
61682
+ studioHint.innerHTML = nudgeMessages[nudgeIndex % nudgeMessages.length];
61683
+ nudgeIndex++;
61684
+ studioHint.classList.add('visible');
61685
+ clearTimeout(nudgeTimer);
61686
+ nudgeTimer = setTimeout(function() { studioHint.classList.remove('visible'); }, 5000);
61687
+ }
61688
+ }
61528
61689
  pointerDown = false;
61529
61690
  draggedNodeId = null;
61530
61691
  canvas.classList.remove('dragging');
@@ -61616,9 +61777,41 @@ path[data-source].port-hover { opacity: 1; }
61616
61777
  var connIndex = [];
61617
61778
  content.querySelectorAll('path[data-source]').forEach(function(p) {
61618
61779
  var src = p.getAttribute('data-source'), tgt = p.getAttribute('data-target');
61619
- connIndex.push({ el: p, src: src, tgt: tgt, srcNode: src.split('.')[0], tgtNode: tgt.split('.')[0] });
61780
+ connIndex.push({ el: p, src: src, tgt: tgt, srcNode: src.split('.')[0], tgtNode: tgt.split('.')[0], scopeOf: p.getAttribute('data-scope') || null });
61620
61781
  });
61621
61782
 
61783
+ // Snapshot of original port positions for reset
61784
+ var origPortPositions = {};
61785
+ for (var pid in portPositions) {
61786
+ origPortPositions[pid] = { cx: portPositions[pid].cx, cy: portPositions[pid].cy };
61787
+ }
61788
+
61789
+ function resetLayout() {
61790
+ for (var nid in nodeOffsets) {
61791
+ var esc = CSS.escape(nid);
61792
+ var nodeG = content.querySelector('.nodes [data-node-id="' + esc + '"]');
61793
+ if (nodeG) nodeG.removeAttribute('transform');
61794
+ var labelG = content.querySelector('[data-label-for="' + esc + '"]');
61795
+ if (labelG) labelG.removeAttribute('transform');
61796
+ allLabelIds.forEach(function(id) {
61797
+ if (id.indexOf(nid + '.') === 0) {
61798
+ var el = labelMap[id];
61799
+ if (el) el.removeAttribute('transform');
61800
+ }
61801
+ });
61802
+ }
61803
+ nodeOffsets = {};
61804
+ for (var pid in origPortPositions) {
61805
+ portPositions[pid] = { cx: origPortPositions[pid].cx, cy: origPortPositions[pid].cy };
61806
+ }
61807
+ connIndex.forEach(function(c) {
61808
+ var sp = portPositions[c.src], tp = portPositions[c.tgt];
61809
+ if (sp && tp) c.el.setAttribute('d', computeConnectionPath(sp.cx, sp.cy, tp.cx, tp.cy));
61810
+ });
61811
+ fitToView();
61812
+ }
61813
+ document.getElementById('btn-reset').addEventListener('click', resetLayout);
61814
+
61622
61815
  // ---- Node drag: moveNode ----
61623
61816
  function moveNode(nodeId, dx, dy) {
61624
61817
  if (!nodeOffsets[nodeId]) nodeOffsets[nodeId] = { dx: 0, dy: 0 };
@@ -61626,9 +61819,18 @@ path[data-source].port-hover { opacity: 1; }
61626
61819
  off.dx += dx; off.dy += dy;
61627
61820
  var tr = 'translate(' + off.dx + ',' + off.dy + ')';
61628
61821
 
61629
- // Move node group
61822
+ // Move node group (if nested inside a scoped parent, subtract parent offset)
61630
61823
  var nodeG = content.querySelector('.nodes [data-node-id="' + CSS.escape(nodeId) + '"]');
61631
- if (nodeG) nodeG.setAttribute('transform', tr);
61824
+ if (nodeG) {
61825
+ var parentNodeG = nodeG.parentElement ? nodeG.parentElement.closest('[data-node-id]') : null;
61826
+ if (parentNodeG) {
61827
+ var parentId = parentNodeG.getAttribute('data-node-id');
61828
+ var parentOff = nodeOffsets[parentId] || { dx: 0, dy: 0 };
61829
+ nodeG.setAttribute('transform', 'translate(' + (off.dx - parentOff.dx) + ',' + (off.dy - parentOff.dy) + ')');
61830
+ } else {
61831
+ nodeG.setAttribute('transform', tr);
61832
+ }
61833
+ }
61632
61834
 
61633
61835
  // Move label
61634
61836
  var labelG = content.querySelector('[data-label-for="' + CSS.escape(nodeId) + '"]');
@@ -61675,11 +61877,20 @@ path[data-source].port-hover { opacity: 1; }
61675
61877
  });
61676
61878
  }
61677
61879
 
61678
- // Recalculate affected connection paths
61880
+ // Recalculate affected connection paths (skip scope connections when parent is dragged \u2014 they move with the group transform)
61679
61881
  connIndex.forEach(function(c) {
61882
+ if (c.scopeOf === nodeId) return;
61680
61883
  if (c.srcNode === nodeId || c.tgtNode === nodeId) {
61681
61884
  var sp = portPositions[c.src], tp = portPositions[c.tgt];
61682
- if (sp && tp) c.el.setAttribute('d', computeConnectionPath(sp.cx, sp.cy, tp.cx, tp.cy));
61885
+ if (sp && tp) {
61886
+ if (c.scopeOf) {
61887
+ // Scope connection paths live inside the parent group; use parent-local coords
61888
+ var pOff = nodeOffsets[c.scopeOf] || { dx: 0, dy: 0 };
61889
+ c.el.setAttribute('d', computeConnectionPath(sp.cx - pOff.dx, sp.cy - pOff.dy, tp.cx - pOff.dx, tp.cy - pOff.dy));
61890
+ } else {
61891
+ c.el.setAttribute('d', computeConnectionPath(sp.cx, sp.cy, tp.cx, tp.cy));
61892
+ }
61893
+ }
61683
61894
  }
61684
61895
  if (nodeG) {
61685
61896
  var children = nodeG.querySelectorAll(':scope > g[data-node-id]');
@@ -61687,7 +61898,14 @@ path[data-source].port-hover { opacity: 1; }
61687
61898
  var childId = childG.getAttribute('data-node-id');
61688
61899
  if (c.srcNode === childId || c.tgtNode === childId) {
61689
61900
  var sp = portPositions[c.src], tp = portPositions[c.tgt];
61690
- if (sp && tp) c.el.setAttribute('d', computeConnectionPath(sp.cx, sp.cy, tp.cx, tp.cy));
61901
+ if (sp && tp) {
61902
+ if (c.scopeOf) {
61903
+ var pOff = nodeOffsets[c.scopeOf] || { dx: 0, dy: 0 };
61904
+ c.el.setAttribute('d', computeConnectionPath(sp.cx - pOff.dx, sp.cy - pOff.dy, tp.cx - pOff.dx, tp.cy - pOff.dy));
61905
+ } else {
61906
+ c.el.setAttribute('d', computeConnectionPath(sp.cx, sp.cy, tp.cx, tp.cy));
61907
+ }
61908
+ }
61691
61909
  }
61692
61910
  });
61693
61911
  }
@@ -96512,7 +96730,7 @@ function displayInstalledPackage(pkg) {
96512
96730
  }
96513
96731
 
96514
96732
  // src/cli/index.ts
96515
- var version2 = true ? "0.10.0" : "0.0.0-dev";
96733
+ var version2 = true ? "0.10.2" : "0.0.0-dev";
96516
96734
  var program2 = new Command();
96517
96735
  program2.name("flow-weaver").description("Flow Weaver Annotations - Compile and validate workflow files").version(version2, "-v, --version", "Output the current version");
96518
96736
  program2.configureOutput({
@@ -170,6 +170,21 @@ path[data-source].port-hover { opacity: 1; }
170
170
  border-radius: 3px; font-family: 'SF Mono', 'Fira Code', monospace;
171
171
  font-size: 12px; background: rgba(255,255,255,0.1);
172
172
  }
173
+
174
+ /* Studio nudge toast */
175
+ #studio-hint {
176
+ position: fixed; bottom: 60px; left: 50%;
177
+ transform: translateX(-50%);
178
+ background: ${surfaceMain}; border: 1px solid ${borderSubtle};
179
+ padding: 8px 16px; border-radius: 8px;
180
+ font-size: 13px; color: ${textMed};
181
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
182
+ z-index: 20; opacity: 0; transition: opacity 0.4s;
183
+ pointer-events: none;
184
+ }
185
+ #studio-hint.visible { opacity: 1; pointer-events: auto; }
186
+ #studio-hint a { color: ${brandAccent}; text-decoration: none; font-weight: 600; }
187
+ #studio-hint a:hover { text-decoration: underline; }
173
188
  </style>
174
189
  </head>
175
190
  <body>
@@ -193,6 +208,12 @@ path[data-source].port-hover { opacity: 1; }
193
208
  <path d="M1 5h12M1 9h12M5 1v12M9 1v12" opacity="0.4"/>
194
209
  </svg>
195
210
  </button>
211
+ <button class="ctrl-btn" id="btn-reset" title="Reset layout" aria-label="Reset layout">
212
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5">
213
+ <path d="M1.5 2v3.5h3.5"/>
214
+ <path d="M2.1 8.5a5 5 0 1 0 .9-4L1.5 5.5"/>
215
+ </svg>
216
+ </button>
196
217
  </div>
197
218
  <div id="info-panel">
198
219
  <h3 id="info-title"></h3>
@@ -203,6 +224,7 @@ path[data-source].port-hover { opacity: 1; }
203
224
  <span>Flow Weaver</span>
204
225
  </a>
205
226
  <div id="scroll-hint">Use <kbd id="mod-key">Ctrl</kbd> + scroll to zoom</div>
227
+ <div id="studio-hint">Like rearranging? <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a> saves your layouts.</div>
206
228
  <script>
207
229
  (function() {
208
230
  'use strict';
@@ -215,6 +237,7 @@ path[data-source].port-hover { opacity: 1; }
215
237
  var infoTitle = document.getElementById('info-title');
216
238
  var infoBody = document.getElementById('info-body');
217
239
  var scrollHint = document.getElementById('scroll-hint');
240
+ var studioHint = document.getElementById('studio-hint');
218
241
 
219
242
  // Parse the original viewBox (diagram bounding box)
220
243
  var vbParts = '${viewBox}'.split(/\\s+/).map(Number);
@@ -286,6 +309,17 @@ path[data-source].port-hover { opacity: 1; }
286
309
 
287
310
  // ---- Pan (drag) + Node drag ----
288
311
  var draggedNodeId = null, dragNodeStart = null, didDragNode = false;
312
+ var dragCount = 0, nudgeIndex = 0, nudgeTimer = null;
313
+ var nudgeMessages = [
314
+ 'Like rearranging? <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a> saves your layouts.',
315
+ 'Changes here are temporary. <a href="https://flowweaver.ai" target="_blank" rel="noopener">Try the Studio</a> to keep them.',
316
+ 'Want to collaborate? <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a> has real-time sharing.',
317
+ 'Build and deploy from the cloud with <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a>.',
318
+ 'This viewer is read-only. <a href="https://flowweaver.ai" target="_blank" rel="noopener">Build workflows</a> in the Studio.',
319
+ 'Version history, diff viewer, rollbacks. All in <a href="https://flowweaver.ai" target="_blank" rel="noopener">the Studio</a>.',
320
+ 'Debug workflows step by step in <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a>.',
321
+ 'Ship faster. <a href="https://flowweaver.ai" target="_blank" rel="noopener">Flow Weaver Studio</a> runs your workflows in the cloud.'
322
+ ];
289
323
 
290
324
  canvas.addEventListener('pointerdown', function(e) {
291
325
  if (e.button !== 0) return;
@@ -344,6 +378,18 @@ path[data-source].port-hover { opacity: 1; }
344
378
  });
345
379
 
346
380
  function endDrag() {
381
+ if (didDragNode) {
382
+ dragCount++;
383
+ var threshold = nudgeIndex === 0 ? 3 : 5;
384
+ if (dragCount >= threshold) {
385
+ dragCount = 0;
386
+ studioHint.innerHTML = nudgeMessages[nudgeIndex % nudgeMessages.length];
387
+ nudgeIndex++;
388
+ studioHint.classList.add('visible');
389
+ clearTimeout(nudgeTimer);
390
+ nudgeTimer = setTimeout(function() { studioHint.classList.remove('visible'); }, 5000);
391
+ }
392
+ }
347
393
  pointerDown = false;
348
394
  draggedNodeId = null;
349
395
  canvas.classList.remove('dragging');
@@ -435,9 +481,41 @@ path[data-source].port-hover { opacity: 1; }
435
481
  var connIndex = [];
436
482
  content.querySelectorAll('path[data-source]').forEach(function(p) {
437
483
  var src = p.getAttribute('data-source'), tgt = p.getAttribute('data-target');
438
- connIndex.push({ el: p, src: src, tgt: tgt, srcNode: src.split('.')[0], tgtNode: tgt.split('.')[0] });
484
+ connIndex.push({ el: p, src: src, tgt: tgt, srcNode: src.split('.')[0], tgtNode: tgt.split('.')[0], scopeOf: p.getAttribute('data-scope') || null });
439
485
  });
440
486
 
487
+ // Snapshot of original port positions for reset
488
+ var origPortPositions = {};
489
+ for (var pid in portPositions) {
490
+ origPortPositions[pid] = { cx: portPositions[pid].cx, cy: portPositions[pid].cy };
491
+ }
492
+
493
+ function resetLayout() {
494
+ for (var nid in nodeOffsets) {
495
+ var esc = CSS.escape(nid);
496
+ var nodeG = content.querySelector('.nodes [data-node-id="' + esc + '"]');
497
+ if (nodeG) nodeG.removeAttribute('transform');
498
+ var labelG = content.querySelector('[data-label-for="' + esc + '"]');
499
+ if (labelG) labelG.removeAttribute('transform');
500
+ allLabelIds.forEach(function(id) {
501
+ if (id.indexOf(nid + '.') === 0) {
502
+ var el = labelMap[id];
503
+ if (el) el.removeAttribute('transform');
504
+ }
505
+ });
506
+ }
507
+ nodeOffsets = {};
508
+ for (var pid in origPortPositions) {
509
+ portPositions[pid] = { cx: origPortPositions[pid].cx, cy: origPortPositions[pid].cy };
510
+ }
511
+ connIndex.forEach(function(c) {
512
+ var sp = portPositions[c.src], tp = portPositions[c.tgt];
513
+ if (sp && tp) c.el.setAttribute('d', computeConnectionPath(sp.cx, sp.cy, tp.cx, tp.cy));
514
+ });
515
+ fitToView();
516
+ }
517
+ document.getElementById('btn-reset').addEventListener('click', resetLayout);
518
+
441
519
  // ---- Node drag: moveNode ----
442
520
  function moveNode(nodeId, dx, dy) {
443
521
  if (!nodeOffsets[nodeId]) nodeOffsets[nodeId] = { dx: 0, dy: 0 };
@@ -445,9 +523,18 @@ path[data-source].port-hover { opacity: 1; }
445
523
  off.dx += dx; off.dy += dy;
446
524
  var tr = 'translate(' + off.dx + ',' + off.dy + ')';
447
525
 
448
- // Move node group
526
+ // Move node group (if nested inside a scoped parent, subtract parent offset)
449
527
  var nodeG = content.querySelector('.nodes [data-node-id="' + CSS.escape(nodeId) + '"]');
450
- if (nodeG) nodeG.setAttribute('transform', tr);
528
+ if (nodeG) {
529
+ var parentNodeG = nodeG.parentElement ? nodeG.parentElement.closest('[data-node-id]') : null;
530
+ if (parentNodeG) {
531
+ var parentId = parentNodeG.getAttribute('data-node-id');
532
+ var parentOff = nodeOffsets[parentId] || { dx: 0, dy: 0 };
533
+ nodeG.setAttribute('transform', 'translate(' + (off.dx - parentOff.dx) + ',' + (off.dy - parentOff.dy) + ')');
534
+ } else {
535
+ nodeG.setAttribute('transform', tr);
536
+ }
537
+ }
451
538
 
452
539
  // Move label
453
540
  var labelG = content.querySelector('[data-label-for="' + CSS.escape(nodeId) + '"]');
@@ -494,11 +581,20 @@ path[data-source].port-hover { opacity: 1; }
494
581
  });
495
582
  }
496
583
 
497
- // Recalculate affected connection paths
584
+ // Recalculate affected connection paths (skip scope connections when parent is dragged — they move with the group transform)
498
585
  connIndex.forEach(function(c) {
586
+ if (c.scopeOf === nodeId) return;
499
587
  if (c.srcNode === nodeId || c.tgtNode === nodeId) {
500
588
  var sp = portPositions[c.src], tp = portPositions[c.tgt];
501
- if (sp && tp) c.el.setAttribute('d', computeConnectionPath(sp.cx, sp.cy, tp.cx, tp.cy));
589
+ if (sp && tp) {
590
+ if (c.scopeOf) {
591
+ // Scope connection paths live inside the parent group; use parent-local coords
592
+ var pOff = nodeOffsets[c.scopeOf] || { dx: 0, dy: 0 };
593
+ c.el.setAttribute('d', computeConnectionPath(sp.cx - pOff.dx, sp.cy - pOff.dy, tp.cx - pOff.dx, tp.cy - pOff.dy));
594
+ } else {
595
+ c.el.setAttribute('d', computeConnectionPath(sp.cx, sp.cy, tp.cx, tp.cy));
596
+ }
597
+ }
502
598
  }
503
599
  if (nodeG) {
504
600
  var children = nodeG.querySelectorAll(':scope > g[data-node-id]');
@@ -506,7 +602,14 @@ path[data-source].port-hover { opacity: 1; }
506
602
  var childId = childG.getAttribute('data-node-id');
507
603
  if (c.srcNode === childId || c.tgtNode === childId) {
508
604
  var sp = portPositions[c.src], tp = portPositions[c.tgt];
509
- if (sp && tp) c.el.setAttribute('d', computeConnectionPath(sp.cx, sp.cy, tp.cx, tp.cy));
605
+ if (sp && tp) {
606
+ if (c.scopeOf) {
607
+ var pOff = nodeOffsets[c.scopeOf] || { dx: 0, dy: 0 };
608
+ c.el.setAttribute('d', computeConnectionPath(sp.cx - pOff.dx, sp.cy - pOff.dy, tp.cx - pOff.dx, tp.cy - pOff.dy));
609
+ } else {
610
+ c.el.setAttribute('d', computeConnectionPath(sp.cx, sp.cy, tp.cx, tp.cy));
611
+ }
612
+ }
510
613
  }
511
614
  });
512
615
  }
@@ -100,12 +100,12 @@ function renderConnection(parts, conn, gradIndex) {
100
100
  const dashAttr = conn.isStepConnection ? '' : ' stroke-dasharray="8 4"';
101
101
  parts.push(` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="3"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}:output" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}:input"/>`);
102
102
  }
103
- function renderScopeConnection(parts, conn, allConnections) {
103
+ function renderScopeConnection(parts, conn, allConnections, parentNodeId) {
104
104
  const gradIndex = allConnections.indexOf(conn);
105
105
  if (gradIndex < 0)
106
106
  return;
107
107
  const dashAttr = conn.isStepConnection ? '' : ' stroke-dasharray="8 4"';
108
- parts.push(` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="2.5"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}:output" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}:input"/>`);
108
+ parts.push(` <path d="${conn.path}" fill="none" stroke="url(#conn-grad-${gradIndex})" stroke-width="2.5"${dashAttr} stroke-linecap="round" data-source="${escapeXml(conn.fromNode)}.${escapeXml(conn.fromPort)}:output" data-target="${escapeXml(conn.toNode)}.${escapeXml(conn.toPort)}:input" data-scope="${escapeXml(parentNodeId)}"/>`);
109
109
  }
110
110
  // ---- Node rendering ----
111
111
  /** Render node body rect + icon */
@@ -147,7 +147,7 @@ function renderScopedContent(parts, node, theme, themeName, allConnections) {
147
147
  parts.push(` <rect x="${scopeX}" y="${scopeY}" width="${scopeW}" height="${scopeH}" rx="4" fill="none" stroke="${theme.scopeAreaStroke}" stroke-width="1" stroke-dasharray="4 2" opacity="0.5"/>`);
148
148
  // Scope connections (before ports so ports appear on top)
149
149
  for (const conn of node.scopeConnections ?? []) {
150
- renderScopeConnection(parts, conn, allConnections);
150
+ renderScopeConnection(parts, conn, allConnections, node.id);
151
151
  }
152
152
  // Scope port dots (before children so dots sit on top of connections)
153
153
  if (scopePorts) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synergenius/flow-weaver",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "Deterministic workflow compiler for AI agents. Compiles to standalone TypeScript, no runtime dependencies.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -145,6 +145,7 @@
145
145
  "@types/js-yaml": "^4.0.9",
146
146
  "@types/node": "^20.11.0",
147
147
  "@types/react": "^19.0.0",
148
+ "@vitest/coverage-v8": "^4.0.18",
148
149
  "esbuild": "^0.27.2",
149
150
  "prettier": "^3.1.1",
150
151
  "rimraf": "6.1.2",