@tangle-network/sandbox-ui 0.135.0 → 0.136.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.
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as triggerNodeIndex, B as fmtTokens, C as COMPACT_NODE_SIZE, D as buildWorkflowGraph, I as shortModel, L as clampPreview, M as worstSeverity, O as isStructuralKind, R as fmtCost, S as statusBorder, _ as emptySlotLabel, a as NodeMark, b as problemSurface, c as RESTING_BORDER, d as STATUS_LABEL, g as edgeColor, h as StatusPill, j as wfEdgeId, l as RESTING_SHADOW, m as StatusFooter, n as classifyOutput, o as ProblemMarker, s as ProblemMessages, t as NodeOutputBody, u as STATUS_COLOR, v as kindAccent, x as problemTitle, y as problemBorder, z as fmtDuration } from "./node-output-
|
|
1
|
+
import { A as triggerNodeIndex, B as fmtTokens, C as COMPACT_NODE_SIZE, D as buildWorkflowGraph, I as shortModel, L as clampPreview, M as worstSeverity, O as isStructuralKind, R as fmtCost, S as statusBorder, _ as emptySlotLabel, a as NodeMark, b as problemSurface, c as RESTING_BORDER, d as STATUS_LABEL, g as edgeColor, h as StatusPill, j as wfEdgeId, l as RESTING_SHADOW, m as StatusFooter, n as classifyOutput, o as ProblemMarker, s as ProblemMessages, t as NodeOutputBody, u as STATUS_COLOR, v as kindAccent, x as problemTitle, y as problemBorder, z as fmtDuration } from "./node-output-fuk0NV0V.js";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { Expand, Maximize2, Minimize2, Plus, Shrink, X } from "lucide-react";
|
|
@@ -48,7 +48,8 @@ const FLOW_GRAPH_OPTION_KEYS = [
|
|
|
48
48
|
"compact",
|
|
49
49
|
"wrap",
|
|
50
50
|
"edges",
|
|
51
|
-
"reserveEdgeInsert"
|
|
51
|
+
"reserveEdgeInsert",
|
|
52
|
+
"itemCounts"
|
|
52
53
|
];
|
|
53
54
|
/**
|
|
54
55
|
* Accept either an options object or a bare `nodeState` map as the second arg, so
|
|
@@ -88,7 +89,8 @@ function buildFlowGraph(yaml, optionsOrNodeState) {
|
|
|
88
89
|
compact,
|
|
89
90
|
...options.wrap ? { wrap: true } : {},
|
|
90
91
|
...options.edges ? { edges: options.edges } : {},
|
|
91
|
-
...options.reserveEdgeInsert ? { reserveEdgeInsert: true } : {}
|
|
92
|
+
...options.reserveEdgeInsert ? { reserveEdgeInsert: true } : {},
|
|
93
|
+
...options.itemCounts ? { itemCounts: options.itemCounts } : {}
|
|
92
94
|
});
|
|
93
95
|
const isLR = direction === "LR";
|
|
94
96
|
const sourcePosition = isLR ? Position.Right : Position.Bottom;
|
|
@@ -406,50 +408,116 @@ function handleOffset(width, height, direction, compact) {
|
|
|
406
408
|
outY: height
|
|
407
409
|
};
|
|
408
410
|
}
|
|
411
|
+
/** Fewer items than this stay a column of lanes; more become a grid. */
|
|
412
|
+
const GRID_MIN_ITEMS = 5;
|
|
413
|
+
const GRID_GAP = 16;
|
|
414
|
+
function itemGrids(graph, direction) {
|
|
415
|
+
const grids = [];
|
|
416
|
+
for (const source of graph.nodes) {
|
|
417
|
+
if (source.data.kind !== "foreach") continue;
|
|
418
|
+
const prefix = `${source.id}-b`;
|
|
419
|
+
const members = graph.nodes.filter((n) => n.id.startsWith(prefix) && /^\d+$/.test(n.id.slice(prefix.length)));
|
|
420
|
+
if (members.length < GRID_MIN_ITEMS) continue;
|
|
421
|
+
const ids = new Set(members.map((n) => n.id));
|
|
422
|
+
const targets = [...new Set(graph.edges.filter((e) => ids.has(e.source) && !ids.has(e.target)).map((e) => e.target))];
|
|
423
|
+
const cellW = Math.max(...members.map((n) => n.width ?? 0));
|
|
424
|
+
const cellH = Math.max(...members.map((n) => n.height ?? 0));
|
|
425
|
+
const cols = direction === "LR" ? Math.ceil(Math.sqrt(members.length * 1.6)) : 2;
|
|
426
|
+
const rows = Math.ceil(members.length / cols);
|
|
427
|
+
grids.push({
|
|
428
|
+
source: source.id,
|
|
429
|
+
members: members.map((n) => n.id),
|
|
430
|
+
targets,
|
|
431
|
+
cols,
|
|
432
|
+
cellW,
|
|
433
|
+
cellH,
|
|
434
|
+
width: cols * cellW + (cols - 1) * GRID_GAP + 2 * FRAME_PAD,
|
|
435
|
+
height: rows * cellH + (rows - 1) * GRID_GAP + 2 * FRAME_PAD + FRAME_CAPTION
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
return grids;
|
|
439
|
+
}
|
|
440
|
+
const gridNodeId = (source) => `grid:${source}`;
|
|
409
441
|
function toElkGraph(graph, direction, wrapAspect, compact = false, foldFailureInputs = true) {
|
|
410
442
|
const spacing = graph.spacing ?? {
|
|
411
443
|
rank: 72,
|
|
412
444
|
cross: 24
|
|
413
445
|
};
|
|
414
446
|
const sinks = foldedSinks(graph, foldFailureInputs);
|
|
447
|
+
const grids = itemGrids(graph, direction);
|
|
448
|
+
const inGrid = new Set(grids.flatMap((g) => g.members));
|
|
449
|
+
const isLR = direction === "LR";
|
|
450
|
+
const nodeChild = (n) => {
|
|
451
|
+
const width = n.width ?? 0;
|
|
452
|
+
const height = n.height ?? 0;
|
|
453
|
+
const at = handleOffset(width, height, direction, compact);
|
|
454
|
+
return {
|
|
455
|
+
id: n.id,
|
|
456
|
+
width,
|
|
457
|
+
height,
|
|
458
|
+
layoutOptions: {
|
|
459
|
+
"elk.portConstraints": "FIXED_POS",
|
|
460
|
+
...sinks.has(n.id) ? { "elk.layered.layering.layerConstraint": "LAST" } : {}
|
|
461
|
+
},
|
|
462
|
+
ports: [{
|
|
463
|
+
id: `${n.id}:in`,
|
|
464
|
+
x: at.inX,
|
|
465
|
+
y: at.inY,
|
|
466
|
+
width: 0,
|
|
467
|
+
height: 0
|
|
468
|
+
}, {
|
|
469
|
+
id: `${n.id}:out`,
|
|
470
|
+
x: at.outX,
|
|
471
|
+
y: at.outY,
|
|
472
|
+
width: 0,
|
|
473
|
+
height: 0
|
|
474
|
+
}]
|
|
475
|
+
};
|
|
476
|
+
};
|
|
477
|
+
const gridChild = (g) => ({
|
|
478
|
+
id: gridNodeId(g.source),
|
|
479
|
+
width: g.width,
|
|
480
|
+
height: g.height,
|
|
481
|
+
layoutOptions: { "elk.portConstraints": "FIXED_POS" },
|
|
482
|
+
ports: [{
|
|
483
|
+
id: `${gridNodeId(g.source)}:in`,
|
|
484
|
+
x: isLR ? 0 : g.width / 2,
|
|
485
|
+
y: isLR ? g.height / 2 : 0,
|
|
486
|
+
width: 0,
|
|
487
|
+
height: 0
|
|
488
|
+
}, {
|
|
489
|
+
id: `${gridNodeId(g.source)}:out`,
|
|
490
|
+
x: isLR ? g.width : g.width / 2,
|
|
491
|
+
y: isLR ? g.height / 2 : g.height,
|
|
492
|
+
width: 0,
|
|
493
|
+
height: 0
|
|
494
|
+
}]
|
|
495
|
+
});
|
|
415
496
|
return {
|
|
416
497
|
id: "root",
|
|
417
498
|
layoutOptions: {
|
|
418
499
|
...elkLayoutOptions(direction, spacing),
|
|
419
500
|
...wrapAspect !== void 0 ? wrapOptions(wrapAspect) : {}
|
|
420
501
|
},
|
|
421
|
-
children: graph.nodes.
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
return
|
|
426
|
-
id: n.id,
|
|
427
|
-
width,
|
|
428
|
-
height,
|
|
429
|
-
layoutOptions: {
|
|
430
|
-
"elk.portConstraints": "FIXED_POS",
|
|
431
|
-
...sinks.has(n.id) ? { "elk.layered.layering.layerConstraint": "LAST" } : {}
|
|
432
|
-
},
|
|
433
|
-
ports: [{
|
|
434
|
-
id: `${n.id}:in`,
|
|
435
|
-
x: at.inX,
|
|
436
|
-
y: at.inY,
|
|
437
|
-
width: 0,
|
|
438
|
-
height: 0
|
|
439
|
-
}, {
|
|
440
|
-
id: `${n.id}:out`,
|
|
441
|
-
x: at.outX,
|
|
442
|
-
y: at.outY,
|
|
443
|
-
width: 0,
|
|
444
|
-
height: 0
|
|
445
|
-
}]
|
|
446
|
-
};
|
|
502
|
+
children: graph.nodes.flatMap((n) => {
|
|
503
|
+
const grid = grids.find((g) => g.members[0] === n.id);
|
|
504
|
+
if (grid) return [gridChild(grid)];
|
|
505
|
+
if (inGrid.has(n.id)) return [];
|
|
506
|
+
return [nodeChild(n)];
|
|
447
507
|
}),
|
|
448
|
-
edges: graph.edges.filter((e) => !sinks.has(e.target)).map((e) => ({
|
|
508
|
+
edges: [...graph.edges.filter((e) => !sinks.has(e.target) && !inGrid.has(e.source) && !inGrid.has(e.target)).map((e) => ({
|
|
449
509
|
id: e.id,
|
|
450
510
|
sources: [`${e.source}:out`],
|
|
451
511
|
targets: [`${e.target}:in`]
|
|
452
|
-
}))
|
|
512
|
+
})), ...grids.flatMap((g) => [{
|
|
513
|
+
id: `${gridNodeId(g.source)}:enter`,
|
|
514
|
+
sources: [`${g.source}:out`],
|
|
515
|
+
targets: [`${gridNodeId(g.source)}:in`]
|
|
516
|
+
}, ...g.targets.filter((t) => !sinks.has(t)).map((t) => ({
|
|
517
|
+
id: `${gridNodeId(g.source)}:leave:${t}`,
|
|
518
|
+
sources: [`${gridNodeId(g.source)}:out`],
|
|
519
|
+
targets: [`${t}:in`]
|
|
520
|
+
}))])]
|
|
453
521
|
};
|
|
454
522
|
}
|
|
455
523
|
/** The middle of a polyline's longest straight run: the one place on a routed
|
|
@@ -492,7 +560,7 @@ function foldedSinks(graph, fold) {
|
|
|
492
560
|
if (!fold) return /* @__PURE__ */ new Set();
|
|
493
561
|
return new Set(graph.nodes.filter((n) => isFailureSink(graph, n.id)).map((n) => n.id));
|
|
494
562
|
}
|
|
495
|
-
function applyElkLayout(graph, laid, foldFailureInputs = true) {
|
|
563
|
+
function applyElkLayout(graph, laid, direction, foldFailureInputs = true) {
|
|
496
564
|
const placed = /* @__PURE__ */ new Map();
|
|
497
565
|
for (const child of laid.children ?? []) {
|
|
498
566
|
if (child.x === void 0 || child.y === void 0) return null;
|
|
@@ -501,6 +569,46 @@ function applyElkLayout(graph, laid, foldFailureInputs = true) {
|
|
|
501
569
|
y: child.y
|
|
502
570
|
});
|
|
503
571
|
}
|
|
572
|
+
const grids = itemGrids(graph, direction);
|
|
573
|
+
const gridFrames = [];
|
|
574
|
+
for (const g of grids) {
|
|
575
|
+
const box = placed.get(gridNodeId(g.source));
|
|
576
|
+
if (!box) return null;
|
|
577
|
+
g.members.forEach((id, k) => {
|
|
578
|
+
const col = k % g.cols;
|
|
579
|
+
const row = Math.floor(k / g.cols);
|
|
580
|
+
placed.set(id, {
|
|
581
|
+
x: box.x + FRAME_PAD + col * (g.cellW + GRID_GAP),
|
|
582
|
+
y: box.y + FRAME_PAD + FRAME_CAPTION + row * (g.cellH + GRID_GAP)
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
const concurrency = graph.nodes.find((n) => n.id === g.source)?.data.config?.maxConcurrency;
|
|
586
|
+
const tileInset = graph.compact && graph.direction === "LR" ? (n) => ((n.width ?? 0) - 76) / 2 : () => 0;
|
|
587
|
+
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
588
|
+
const connectors = (laid.edges ?? []).filter((e) => e.id.startsWith(`${gridNodeId(g.source)}:`)).map((e) => {
|
|
589
|
+
const points = edgePoints(e).map((p) => ({
|
|
590
|
+
x: p.x,
|
|
591
|
+
y: p.y
|
|
592
|
+
}));
|
|
593
|
+
const from = nodeById.get(e.sources[0].replace(/:out$/, ""));
|
|
594
|
+
const to = nodeById.get(e.targets[0].replace(/:in$/, ""));
|
|
595
|
+
if (points.length >= 2 && from) points[0].x -= tileInset(from);
|
|
596
|
+
if (points.length >= 2 && to) points[points.length - 1].x += tileInset(to);
|
|
597
|
+
return { points };
|
|
598
|
+
}).filter((c) => c.points.length >= 2);
|
|
599
|
+
gridFrames.push({
|
|
600
|
+
id: `frame:${gridNodeId(g.source)}`,
|
|
601
|
+
x: box.x,
|
|
602
|
+
y: box.y,
|
|
603
|
+
width: g.width,
|
|
604
|
+
height: g.height,
|
|
605
|
+
lanes: g.members.length,
|
|
606
|
+
agents: g.members.filter((id) => graph.nodes.find((n) => n.id === id)?.data.kind === "agent.run").length,
|
|
607
|
+
forEach: typeof concurrency === "number" ? { concurrency } : {},
|
|
608
|
+
connectors
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
const inGrid = new Set(grids.flatMap((g) => g.members));
|
|
504
612
|
if (graph.nodes.some((n) => !placed.has(n.id))) return null;
|
|
505
613
|
const routed = /* @__PURE__ */ new Map();
|
|
506
614
|
for (const e of laid.edges ?? []) routed.set(e.id, e);
|
|
@@ -519,10 +627,18 @@ function applyElkLayout(graph, laid, foldFailureInputs = true) {
|
|
|
519
627
|
};
|
|
520
628
|
});
|
|
521
629
|
const edges = graph.edges.map((e) => {
|
|
522
|
-
if (
|
|
630
|
+
if (inGrid.has(e.source) || inGrid.has(e.target)) return {
|
|
523
631
|
...e,
|
|
524
632
|
hidden: true
|
|
525
633
|
};
|
|
634
|
+
if (sinkInputs.has(e.target)) return {
|
|
635
|
+
...e,
|
|
636
|
+
hidden: true,
|
|
637
|
+
...e.data ? { data: {
|
|
638
|
+
...e.data,
|
|
639
|
+
foldedFailureInput: true
|
|
640
|
+
} } : {}
|
|
641
|
+
};
|
|
526
642
|
const r = routed.get(e.id);
|
|
527
643
|
const points = r ? edgePoints(r) : [];
|
|
528
644
|
if (points.length < 2) return e;
|
|
@@ -551,7 +667,7 @@ function applyElkLayout(graph, laid, foldFailureInputs = true) {
|
|
|
551
667
|
edges,
|
|
552
668
|
engine: "elk"
|
|
553
669
|
};
|
|
554
|
-
const groups = parallelFrames(laidOut);
|
|
670
|
+
const groups = [...gridFrames, ...parallelFrames(laidOut)];
|
|
555
671
|
return groups.length > 0 ? {
|
|
556
672
|
...laidOut,
|
|
557
673
|
groups
|
|
@@ -573,6 +689,7 @@ const FRAME_CAPTION = 18;
|
|
|
573
689
|
* claiming an unrelated step is worse than no frame.
|
|
574
690
|
*/
|
|
575
691
|
function parallelFrames(graph) {
|
|
692
|
+
const incomingSource = (key) => key.includes("|") ? null : key;
|
|
576
693
|
const forward = graph.edges.filter((e) => !e.data?.backEdge && !e.hidden);
|
|
577
694
|
const preds = /* @__PURE__ */ new Map();
|
|
578
695
|
const succs = /* @__PURE__ */ new Map();
|
|
@@ -620,7 +737,11 @@ function parallelFrames(graph) {
|
|
|
620
737
|
const ny1 = n.position.y + (n.height ?? 0);
|
|
621
738
|
return n.position.x < x1 && nx1 > x0 && n.position.y < y1 && ny1 > y0;
|
|
622
739
|
})) continue;
|
|
740
|
+
const source = incomingSource(key);
|
|
741
|
+
const sourceNode = source ? byId.get(source) : void 0;
|
|
742
|
+
const concurrency = sourceNode?.data.config?.maxConcurrency;
|
|
623
743
|
frames.push({
|
|
744
|
+
...sourceNode?.data.kind === "foreach" ? { forEach: typeof concurrency === "number" ? { concurrency } : {} } : {},
|
|
624
745
|
id: `frame:${key}`,
|
|
625
746
|
x: x0,
|
|
626
747
|
y: y0,
|
|
@@ -641,11 +762,11 @@ async function layoutWithElk(graph, direction, compact, canvasAspect = DEFAULT_C
|
|
|
641
762
|
const elk = await loadElk();
|
|
642
763
|
if (!elk) return null;
|
|
643
764
|
try {
|
|
644
|
-
const plain = applyElkLayout(graph, await elk.layout(toElkGraph(graph, direction, void 0, compact, fold)), fold);
|
|
765
|
+
const plain = applyElkLayout(graph, await elk.layout(toElkGraph(graph, direction, void 0, compact, fold)), direction, fold);
|
|
645
766
|
if (!plain) return null;
|
|
646
767
|
const plainAspect = boundsAspect(plain);
|
|
647
|
-
if (direction !== "LR" || graph
|
|
648
|
-
const wrapped = applyElkLayout(graph, await elk.layout(toElkGraph(graph, direction, canvasAspect, compact, fold)), fold);
|
|
768
|
+
if (direction !== "LR" || laidOutNodeCount(graph) < WRAP_MIN_NODES || plainAspect < canvasAspect * WRAP_WHEN_WIDER) return plain;
|
|
769
|
+
const wrapped = applyElkLayout(graph, await elk.layout(toElkGraph(graph, direction, canvasAspect, compact, fold)), direction, fold);
|
|
649
770
|
if (!wrapped) return plain;
|
|
650
771
|
return fitScale(wrapped, canvasAspect) > fitScale(plain, canvasAspect) ? wrapped : plain;
|
|
651
772
|
} catch {
|
|
@@ -658,6 +779,11 @@ const DEFAULT_CANVAS_ASPECT = 2.2;
|
|
|
658
779
|
/** How much wider than its canvas a single band must be before wrapping is
|
|
659
780
|
* tried. */
|
|
660
781
|
const WRAP_WHEN_WIDER = 1.6;
|
|
782
|
+
/** The nodes ELK places: a grid of items counts once. */
|
|
783
|
+
function laidOutNodeCount(graph) {
|
|
784
|
+
const grids = itemGrids(graph, "LR");
|
|
785
|
+
return graph.nodes.length - grids.reduce((sum, g) => sum + g.members.length, 0) + grids.length;
|
|
786
|
+
}
|
|
661
787
|
/** Below this many nodes a graph reads better as one band: cut into rows, a
|
|
662
788
|
* six-step loop sends its edges across the row break for no gain in size. */
|
|
663
789
|
const WRAP_MIN_NODES = 12;
|
|
@@ -812,12 +938,18 @@ function graphBounds(graph) {
|
|
|
812
938
|
function openingViewport(graph, width, height, compact, direction) {
|
|
813
939
|
const bounds = graphBounds(graph);
|
|
814
940
|
const view = framingViewport(bounds, width, height, compact, elkFloor(graph, direction));
|
|
941
|
+
if (direction !== "TB") return view;
|
|
942
|
+
const y = bounds.height * view.zoom > height ? Math.max(view.y, 44 - bounds.y * view.zoom) : view.y;
|
|
815
943
|
const entry = graph.nodes[0];
|
|
816
|
-
if (
|
|
944
|
+
if (!entry || bounds.width * view.zoom <= width) return {
|
|
945
|
+
...view,
|
|
946
|
+
y
|
|
947
|
+
};
|
|
817
948
|
const entryCenter = entry.position.x + (entry.width ?? 0) / 2;
|
|
818
949
|
return {
|
|
819
950
|
...view,
|
|
820
|
-
x: width / 2 - entryCenter * view.zoom
|
|
951
|
+
x: width / 2 - entryCenter * view.zoom,
|
|
952
|
+
y
|
|
821
953
|
};
|
|
822
954
|
}
|
|
823
955
|
/**
|
|
@@ -1551,13 +1683,43 @@ function useCanvasAspect() {
|
|
|
1551
1683
|
/** "4 agents in parallel" when each lane is one agent; "4 parallel lanes ·
|
|
1552
1684
|
* 8 agents" when a lane is a chain (a reviewer and its verifier). */
|
|
1553
1685
|
function parallelCaption(frame) {
|
|
1686
|
+
if (frame.forEach) {
|
|
1687
|
+
const at = frame.forEach.concurrency;
|
|
1688
|
+
return at !== void 0 && at < frame.lanes ? `${frame.lanes} items · ${at} at a time` : `${frame.lanes} items in parallel`;
|
|
1689
|
+
}
|
|
1554
1690
|
if (frame.agents > frame.lanes) return `${frame.lanes} parallel lanes · ${frame.agents} agents`;
|
|
1555
1691
|
return frame.agents === frame.lanes ? `${frame.lanes} agents in parallel` : `${frame.lanes} in parallel`;
|
|
1556
1692
|
}
|
|
1557
1693
|
/** Frames behind parallel fan-outs, drawn in flow coordinates under the
|
|
1558
1694
|
* nodes. Pointer-transparent: a frame annotates, it is never a target. */
|
|
1559
1695
|
function ParallelFrames({ frames }) {
|
|
1560
|
-
|
|
1696
|
+
const connectors = frames.flatMap((f) => f.connectors ?? []);
|
|
1697
|
+
return /* @__PURE__ */ jsxs(ViewportPortal, { children: [connectors.length > 0 && /* @__PURE__ */ jsxs("svg", {
|
|
1698
|
+
"aria-hidden": true,
|
|
1699
|
+
className: "pointer-events-none absolute top-0 left-0 overflow-visible",
|
|
1700
|
+
style: { zIndex: -1 },
|
|
1701
|
+
width: 1,
|
|
1702
|
+
height: 1,
|
|
1703
|
+
children: [/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("marker", {
|
|
1704
|
+
id: "wf-grid-arrow",
|
|
1705
|
+
viewBox: "0 0 10 10",
|
|
1706
|
+
refX: "9",
|
|
1707
|
+
refY: "5",
|
|
1708
|
+
markerWidth: "7",
|
|
1709
|
+
markerHeight: "7",
|
|
1710
|
+
orient: "auto-start-reverse",
|
|
1711
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1712
|
+
d: "M 0 0 L 10 5 L 0 10 z",
|
|
1713
|
+
fill: GRID_CONNECTOR_COLOR
|
|
1714
|
+
})
|
|
1715
|
+
}) }), connectors.map((c, k) => /* @__PURE__ */ jsx("path", {
|
|
1716
|
+
d: roundedPolyline(c.points),
|
|
1717
|
+
fill: "none",
|
|
1718
|
+
stroke: GRID_CONNECTOR_COLOR,
|
|
1719
|
+
strokeWidth: 1.75,
|
|
1720
|
+
markerEnd: "url(#wf-grid-arrow)"
|
|
1721
|
+
}, k))]
|
|
1722
|
+
}), frames.map((frame) => /* @__PURE__ */ jsx("div", {
|
|
1561
1723
|
"data-testid": "wf-parallel-frame",
|
|
1562
1724
|
className: "pointer-events-none absolute rounded-xl border border-primary/30 border-dashed bg-primary/[0.04]",
|
|
1563
1725
|
style: {
|
|
@@ -1570,7 +1732,7 @@ function ParallelFrames({ frames }) {
|
|
|
1570
1732
|
className: "absolute top-1 left-2.5 font-medium text-[10px] text-primary/80 uppercase tracking-wide",
|
|
1571
1733
|
children: parallelCaption(frame)
|
|
1572
1734
|
})
|
|
1573
|
-
}, frame.id)) });
|
|
1735
|
+
}, frame.id))] });
|
|
1574
1736
|
}
|
|
1575
1737
|
/** Below this zoom a compact node's name is too small to read, so it grows to
|
|
1576
1738
|
* hold a readable size on screen; the tile, the layout and the edges keep
|
|
@@ -1594,6 +1756,25 @@ function useCompactTitleScale(enabled) {
|
|
|
1594
1756
|
return Math.min(TITLE_MAX_SCALE, Math.round(TITLE_READABLE_ZOOM / zoom * 10) / 10);
|
|
1595
1757
|
}, [enabled]));
|
|
1596
1758
|
}
|
|
1759
|
+
/** The highest leaf index per fan-out in a run state, as a stable string.
|
|
1760
|
+
* The model applies counts to `foreach` steps only; a `parallel` already
|
|
1761
|
+
* draws every branch it declares. Null when no step reported more than one
|
|
1762
|
+
* item. */
|
|
1763
|
+
function foreachItemCountsKey(nodeState) {
|
|
1764
|
+
if (!nodeState) return null;
|
|
1765
|
+
const counts = {};
|
|
1766
|
+
for (const id of Object.keys(nodeState)) {
|
|
1767
|
+
const match = /^a(\d+)-b(\d+)$/.exec(id);
|
|
1768
|
+
if (!match) continue;
|
|
1769
|
+
const action = Number(match[1]);
|
|
1770
|
+
counts[action] = Math.max(counts[action] ?? 0, Number(match[2]) + 1);
|
|
1771
|
+
}
|
|
1772
|
+
const wide = Object.entries(counts).filter(([, n]) => n > 1);
|
|
1773
|
+
return wide.length > 0 ? JSON.stringify(Object.fromEntries(wide)) : null;
|
|
1774
|
+
}
|
|
1775
|
+
/** The resting edge colour (edgeColor with no run state), for the lines a
|
|
1776
|
+
* grid frame draws itself. */
|
|
1777
|
+
const GRID_CONNECTOR_COLOR = "color-mix(in srgb, hsl(var(--muted-foreground)) 38%, transparent)";
|
|
1597
1778
|
/** Past this many nodes the full canvas carries a minimap, so a reader zoomed
|
|
1598
1779
|
* into one corner of a large graph can see where they are in it. Below it
|
|
1599
1780
|
* the wrapped layout fits the canvas whole, and a minimap would only cover
|
|
@@ -1817,13 +1998,16 @@ function WorkflowGraph({ yaml, variant = "full", direction: directionProp, defau
|
|
|
1817
1998
|
const framedRef = useRef(false);
|
|
1818
1999
|
const [framingCue, setFramingCue] = useState(0);
|
|
1819
2000
|
const hasRunOverlay = nodeState !== void 0;
|
|
2001
|
+
const itemCountsKey = useMemo(() => foreachItemCountsKey(nodeState), [nodeState]);
|
|
2002
|
+
const itemCounts = useMemo(() => itemCountsKey ? JSON.parse(itemCountsKey) : void 0, [itemCountsKey]);
|
|
1820
2003
|
const { graph: structural, ready: laidOut } = useElkLayout(useMemo(() => buildFlowGraph(yaml, {
|
|
1821
2004
|
nodeState: hasRunOverlay ? {} : void 0,
|
|
1822
2005
|
direction,
|
|
1823
2006
|
compact,
|
|
1824
2007
|
wrap,
|
|
1825
2008
|
reserveEdgeInsert: insertArmed,
|
|
1826
|
-
...declaredEdges ? { edges: declaredEdges } : {}
|
|
2009
|
+
...declaredEdges ? { edges: declaredEdges } : {},
|
|
2010
|
+
...itemCounts ? { itemCounts } : {}
|
|
1827
2011
|
}), [
|
|
1828
2012
|
yaml,
|
|
1829
2013
|
hasRunOverlay,
|
|
@@ -1831,7 +2015,8 @@ function WorkflowGraph({ yaml, variant = "full", direction: directionProp, defau
|
|
|
1831
2015
|
compact,
|
|
1832
2016
|
wrap,
|
|
1833
2017
|
insertArmed,
|
|
1834
|
-
declaredEdges
|
|
2018
|
+
declaredEdges,
|
|
2019
|
+
itemCounts
|
|
1835
2020
|
]), direction, compact, canvasAspect, !editable);
|
|
1836
2021
|
const shownCompact = structural.compact ?? compact;
|
|
1837
2022
|
const shownDirection = structural.direction ?? direction;
|
|
@@ -1846,7 +2031,7 @@ function WorkflowGraph({ yaml, variant = "full", direction: directionProp, defau
|
|
|
1846
2031
|
const styledEdges = useMemo(() => {
|
|
1847
2032
|
return decorateAuthoringEdges(buildStyledEdges(structural.edges, nodeState, maxNodeVisits), problemIndex.byEdge, (edge) => insertArmed && isEditableEdge(edge), hasRunOverlay).map((e) => ({
|
|
1848
2033
|
...e,
|
|
1849
|
-
...e.hidden && nodeState?.[e.source]?.status === "failed" ? { hidden: false } : {},
|
|
2034
|
+
...e.hidden && e.data?.foldedFailureInput && nodeState?.[e.source]?.status === "failed" ? { hidden: false } : {},
|
|
1850
2035
|
deletable: editable ? isEditableEdge(e) : false
|
|
1851
2036
|
}));
|
|
1852
2037
|
}, [
|
|
@@ -1245,13 +1245,18 @@ function buildWorkflowGraph(yaml, options) {
|
|
|
1245
1245
|
addNode(id, data, rank, reserveRunState);
|
|
1246
1246
|
if (!declared) for (const from of prevExits) addEdge(from, id, branchLeafIds.has(from) ? "join" : "spine");
|
|
1247
1247
|
const rec = asRecord(action);
|
|
1248
|
-
const children = "parallel" in rec ? Array.isArray(asRecord(rec.parallel).branches) ? asRecord(rec.parallel).branches : [] : "foreach" in rec ?
|
|
1248
|
+
const children = "parallel" in rec ? Array.isArray(asRecord(rec.parallel).branches) ? asRecord(rec.parallel).branches : [] : "foreach" in rec ? Array.from({ length: asRecord(rec.foreach).do ? Math.max(1, options?.itemCounts?.[i] ?? 1) : 0 }, () => asRecord(rec.foreach).do) : [];
|
|
1249
1249
|
if (children.length > 0) {
|
|
1250
1250
|
const branchRank = rank + 1;
|
|
1251
1251
|
const branchIds = [];
|
|
1252
|
+
const items = "foreach" in rec && children.length > 1;
|
|
1252
1253
|
children.forEach((child, j) => {
|
|
1253
1254
|
const cid = branchNodeId(i, j);
|
|
1254
|
-
|
|
1255
|
+
const data = describeAction(child);
|
|
1256
|
+
addNode(cid, items ? {
|
|
1257
|
+
...data,
|
|
1258
|
+
subtitle: `Item ${j + 1} of ${children.length}`
|
|
1259
|
+
} : data, branchRank, reserveRunState);
|
|
1255
1260
|
addEdge(id, cid, "fork");
|
|
1256
1261
|
branchIds.push(cid);
|
|
1257
1262
|
branchLeafIds.add(cid);
|
package/dist/workflows.d.ts
CHANGED
|
@@ -320,6 +320,13 @@ interface BuildWorkflowGraphOptions {
|
|
|
320
320
|
* measured afterwards. Defaults to `false`.
|
|
321
321
|
*/
|
|
322
322
|
reserveEdgeInsert?: boolean;
|
|
323
|
+
/**
|
|
324
|
+
* How many items a settled `foreach` ran, by its action index. A foreach
|
|
325
|
+
* is one template step until it has run; then each item is a node of its
|
|
326
|
+
* own, so a fan-out over fourteen issues reads as fourteen lanes with their
|
|
327
|
+
* own status and cost rather than one tile with a count.
|
|
328
|
+
*/
|
|
329
|
+
itemCounts?: Readonly<Record<number, number>>;
|
|
323
330
|
}
|
|
324
331
|
/** Build a positioned graph from a workflow YAML string. Never throws —
|
|
325
332
|
* malformed YAML or an empty definition returns an `error` the UI can fall
|
package/dist/workflows.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as modelBrandFor, t as ModelBrandStack } from "./model-brand-BSr9on8C.js";
|
|
2
|
-
import { A as triggerNodeIndex, D as buildWorkflowGraph, E as branchNodeId, F as parseActionPath, I as shortModel, N as actionPathLabel, P as humanizeIdentifier, T as actionNodeId, a as NodeMark, d as STATUS_LABEL, f as STATUS_PILL, h as StatusPill, i as KIND_ICON, k as triggerNodeId, n as classifyOutput, p as STATUS_TONE, r as condenseText, t as NodeOutputBody, u as STATUS_COLOR, w as TRIGGER_NODE_ID } from "./node-output-
|
|
2
|
+
import { A as triggerNodeIndex, D as buildWorkflowGraph, E as branchNodeId, F as parseActionPath, I as shortModel, N as actionPathLabel, P as humanizeIdentifier, T as actionNodeId, a as NodeMark, d as STATUS_LABEL, f as STATUS_PILL, h as StatusPill, i as KIND_ICON, k as triggerNodeId, n as classifyOutput, p as STATUS_TONE, r as condenseText, t as NodeOutputBody, u as STATUS_COLOR, w as TRIGGER_NODE_ID } from "./node-output-fuk0NV0V.js";
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
import { Component, Suspense, lazy, useMemo, useState } from "react";
|
|
5
5
|
//#region src/workflows/WorkflowGraphLazy.tsx
|
|
@@ -30,7 +30,7 @@ function retryImport(factory, retries = 2, delayMs = 350) {
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
function lazyWorkflowGraph() {
|
|
33
|
-
return lazy(() => retryImport(() => import("./WorkflowGraph-
|
|
33
|
+
return lazy(() => retryImport(() => import("./WorkflowGraph-BcuJ0kgt.js").then((m) => ({ default: m.WorkflowGraph }))));
|
|
34
34
|
}
|
|
35
35
|
var GraphErrorBoundary = class extends Component {
|
|
36
36
|
state = { hasError: false };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/sandbox-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.136.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Unified UI component library for Tangle Sandbox — primitives, chat, dashboard, terminal, editor, and workspace components",
|
|
6
6
|
"repository": {
|