@tangle-network/sandbox-ui 0.91.5 → 0.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,8 +10,9 @@ import {
10
10
  fmtDuration,
11
11
  fmtTokens,
12
12
  providerLabel,
13
- shortModel
14
- } from "./chunk-2NS724P4.js";
13
+ shortModel,
14
+ triggerNodeIndex
15
+ } from "./chunk-YOSNN3QU.js";
15
16
  import {
16
17
  ModelBrandStack,
17
18
  modelBrandFor
@@ -22,8 +23,11 @@ import "./chunk-CZ7CSFO4.js";
22
23
  // src/workflows/WorkflowGraph.tsx
23
24
  import {
24
25
  Background,
26
+ BaseEdge,
25
27
  Controls,
28
+ EdgeLabelRenderer,
26
29
  getNodesBounds,
30
+ getSmoothStepPath,
27
31
  getViewportForBounds,
28
32
  Handle,
29
33
  MarkerType,
@@ -47,6 +51,7 @@ import {
47
51
 
48
52
  // src/workflows/flow-graph.ts
49
53
  import { Position } from "@xyflow/react";
54
+ var WF_EDGE_TYPE = "wfEdge";
50
55
  function isNodeStateMap(arg) {
51
56
  const values = Object.values(arg);
52
57
  return values.length > 0 && values.every(
@@ -55,7 +60,7 @@ function isNodeStateMap(arg) {
55
60
  }
56
61
  function normalizeFlowGraphOptions(arg) {
57
62
  if (!arg) return {};
58
- if ("nodeState" in arg || "direction" in arg || "compact" in arg) {
63
+ if ("nodeState" in arg || "direction" in arg || "compact" in arg || "edges" in arg) {
59
64
  return arg;
60
65
  }
61
66
  return isNodeStateMap(arg) ? { nodeState: arg } : {};
@@ -68,7 +73,8 @@ function buildFlowGraph(yaml, optionsOrNodeState) {
68
73
  const graph = buildWorkflowGraph(yaml, {
69
74
  reserveRunState: nodeState !== void 0,
70
75
  direction,
71
- compact
76
+ compact,
77
+ ...options.edges ? { edges: options.edges } : {}
72
78
  });
73
79
  const isLR = direction === "LR";
74
80
  const sourcePosition = isLR ? Position.Right : Position.Bottom;
@@ -82,6 +88,13 @@ function buildFlowGraph(yaml, optionsOrNodeState) {
82
88
  position: n.position,
83
89
  width: n.width,
84
90
  height: n.height,
91
+ // A node is a `do` entry, and removing one is a list edit — never a
92
+ // canvas gesture. This matters most on an EDITABLE canvas, where the
93
+ // delete key is armed for edges: React Flow deletes a selected node
94
+ // together with every edge touching it, so a node left deletable would
95
+ // vanish from the canvas AND report each of its edges as removed,
96
+ // asking the host to drop declared edges nobody touched.
97
+ deletable: false,
85
98
  // Fix the DOM box to the laid-out size so the card can't grow past its
86
99
  // reserved space and overlap a neighbour (see model.ts nodeHeight).
87
100
  style: { width: n.width, height: n.height },
@@ -90,12 +103,20 @@ function buildFlowGraph(yaml, optionsOrNodeState) {
90
103
  data: nodeState?.[n.id] ? { ...n.data, state: nodeState[n.id] } : n.data
91
104
  })
92
105
  ),
93
- edges: graph.edges.map((e) => ({
94
- id: e.id,
95
- source: e.source,
96
- target: e.target,
97
- type: "smoothstep"
98
- }))
106
+ edges: graph.edges.map((e) => {
107
+ const decorated = e.whenLabel !== void 0 || e.backEdge === true;
108
+ return {
109
+ id: e.id,
110
+ source: e.source,
111
+ target: e.target,
112
+ type: decorated ? WF_EDGE_TYPE : "smoothstep",
113
+ data: {
114
+ kind: e.kind,
115
+ ...e.whenLabel !== void 0 ? { whenLabel: e.whenLabel } : {},
116
+ ...e.backEdge === true ? { backEdge: true } : {}
117
+ }
118
+ };
119
+ })
99
120
  };
100
121
  }
101
122
  function sameRunState(a, b) {
@@ -248,10 +269,13 @@ import {
248
269
  Bell,
249
270
  Box,
250
271
  Cable,
272
+ Camera,
251
273
  Circle,
252
274
  Clock,
275
+ Code,
253
276
  GitBranch,
254
277
  Repeat,
278
+ ScanSearch,
255
279
  Sparkles,
256
280
  UserCheck,
257
281
  Webhook,
@@ -335,7 +359,11 @@ function progressFill(status) {
335
359
  }
336
360
  var KIND_ICON = {
337
361
  schedule: Clock,
362
+ // Both wake the workflow on an inbound POST, so they share the mark. A
363
+ // `provider_event` normally shows its provider's LOGO instead and reaches this
364
+ // glyph only when the connector has none.
338
365
  provider_event: Webhook,
366
+ webhook: Webhook,
339
367
  trigger: Zap,
340
368
  "agent.run": Sparkles,
341
369
  "integration.invoke": Cable,
@@ -343,7 +371,10 @@ var KIND_ICON = {
343
371
  parallel: GitBranch,
344
372
  foreach: Repeat,
345
373
  decision: UserCheck,
346
- "sandbox.spawn": Box
374
+ "sandbox.spawn": Box,
375
+ "sandbox.snapshot": Camera,
376
+ "script.run": Code,
377
+ "trace.analyze": ScanSearch
347
378
  };
348
379
  function NodeMark({
349
380
  kind,
@@ -421,6 +452,15 @@ function useColorMode() {
421
452
  }
422
453
  var DirectionContext = createContext("LR");
423
454
  var DensityContext = createContext(false);
455
+ var SelectedNodeContext = createContext(void 0);
456
+ var SELECTION_OUTLINE = {
457
+ outline: "2px solid hsl(var(--primary))",
458
+ outlineOffset: 2
459
+ };
460
+ var ConnectableContext = createContext(false);
461
+ function isEditableEdge(edge) {
462
+ return edge.data?.kind !== "fork" && triggerNodeIndex(edge.source) === null;
463
+ }
424
464
  function StatusFooter({
425
465
  status,
426
466
  rounds,
@@ -475,6 +515,7 @@ function StatusPill({ status }) {
475
515
  );
476
516
  }
477
517
  var HANDLE_CLASS = "!h-2 !w-2 !min-w-0 !border-0 !bg-transparent opacity-0";
518
+ var HANDLE_CONNECTABLE_CLASS = "!h-2.5 !w-2.5 !min-w-0 !rounded-full !border !border-border !bg-muted-foreground/70 opacity-100 transition-opacity hover:!bg-primary";
478
519
  function compactHandleStyle(position, isLR) {
479
520
  const tileLeft = isLR ? (COMPACT_NODE_SIZE.width - COMPACT_TILE) / 2 : 0;
480
521
  const center = COMPACT_TILE / 2;
@@ -496,11 +537,14 @@ function compactHandleStyle(position, isLR) {
496
537
  return anchor(tileLeft + center, COMPACT_TILE);
497
538
  }
498
539
  }
499
- function WorkflowNode({ data }) {
540
+ function WorkflowNode({ id, data }) {
500
541
  const d = data;
501
542
  const state = d.state;
502
543
  const direction = useContext(DirectionContext);
503
544
  const compact = useContext(DensityContext);
545
+ const connectable = useContext(ConnectableContext);
546
+ const hostSelection = useContext(SelectedNodeContext);
547
+ const selected = hostSelection !== void 0 && hostSelection === id;
504
548
  const isLR = direction === "LR";
505
549
  const targetPos = isLR ? Position2.Left : Position2.Top;
506
550
  const sourcePos = isLR ? Position2.Right : Position2.Bottom;
@@ -527,13 +571,14 @@ function WorkflowNode({ data }) {
527
571
  );
528
572
  return shape.kind === "empty" ? null : { shape, tone: source.tone, label: source.label };
529
573
  })();
574
+ const handleClass = connectable ? HANDLE_CONNECTABLE_CLASS : HANDLE_CLASS;
530
575
  const handles = /* @__PURE__ */ jsxs2(Fragment, { children: [
531
576
  !d.isRoot && /* @__PURE__ */ jsx3(
532
577
  Handle,
533
578
  {
534
579
  type: "target",
535
580
  position: targetPos,
536
- className: HANDLE_CLASS,
581
+ className: handleClass,
537
582
  style: compact ? compactHandleStyle(targetPos, isLR) : void 0
538
583
  }
539
584
  ),
@@ -542,7 +587,7 @@ function WorkflowNode({ data }) {
542
587
  {
543
588
  type: "source",
544
589
  position: sourcePos,
545
- className: HANDLE_CLASS,
590
+ className: handleClass,
546
591
  style: compact ? compactHandleStyle(sourcePos, isLR) : void 0
547
592
  }
548
593
  )
@@ -563,7 +608,10 @@ function WorkflowNode({ data }) {
563
608
  height: COMPACT_TILE,
564
609
  ...state ? statusBorder(state.status) : {
565
610
  borderColor: `color-mix(in srgb, ${accent} 40%, hsl(var(--border)))`
566
- }
611
+ },
612
+ // The TILE is the compact node (the name beside it is unboxed), so
613
+ // the selection ring belongs to it and not to the wider box.
614
+ ...selected ? SELECTION_OUTLINE : {}
567
615
  },
568
616
  children: [
569
617
  /* @__PURE__ */ jsx3("span", { className: "wf-node-body-in flex items-center justify-center", children: /* @__PURE__ */ jsx3(
@@ -624,8 +672,11 @@ function WorkflowNode({ data }) {
624
672
  "div",
625
673
  {
626
674
  className: "relative flex h-full w-full flex-col overflow-hidden rounded-xl border bg-card shadow-sm transition-colors",
627
- style: state ? statusBorder(state.status) : {
628
- borderColor: `color-mix(in srgb, ${accent} 40%, hsl(var(--border)))`
675
+ style: {
676
+ ...state ? statusBorder(state.status) : {
677
+ borderColor: `color-mix(in srgb, ${accent} 40%, hsl(var(--border)))`
678
+ },
679
+ ...selected ? SELECTION_OUTLINE : {}
629
680
  },
630
681
  children: [
631
682
  handles,
@@ -695,22 +746,88 @@ function WorkflowNode({ data }) {
695
746
  }
696
747
  );
697
748
  }
749
+ var EDGE_CHIP_CLASS = "rounded-full border border-border bg-card/90 px-1.5 py-[1px] text-[10px] leading-tight backdrop-blur";
750
+ function WfEdgeRenderer({
751
+ id,
752
+ sourceX,
753
+ sourceY,
754
+ targetX,
755
+ targetY,
756
+ sourcePosition,
757
+ targetPosition,
758
+ data,
759
+ markerEnd,
760
+ style
761
+ }) {
762
+ const [path, labelX, labelY] = getSmoothStepPath({
763
+ sourceX,
764
+ sourceY,
765
+ sourcePosition,
766
+ targetX,
767
+ targetY,
768
+ targetPosition
769
+ });
770
+ const backEdge = data?.backEdge === true;
771
+ const whenLabel = data?.whenLabel;
772
+ const visits = data?.maxNodeVisits;
773
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
774
+ /* @__PURE__ */ jsx3(BaseEdge, { id, path, markerEnd, style }),
775
+ /* @__PURE__ */ jsx3(EdgeLabelRenderer, { children: /* @__PURE__ */ jsxs2(
776
+ "div",
777
+ {
778
+ className: "nodrag nopan absolute flex flex-col items-center gap-0.5",
779
+ style: {
780
+ transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`
781
+ },
782
+ children: [
783
+ whenLabel && /* @__PURE__ */ jsx3(
784
+ "span",
785
+ {
786
+ title: whenLabel,
787
+ className: `${EDGE_CHIP_CLASS} max-w-40 truncate text-muted-foreground`,
788
+ children: whenLabel
789
+ }
790
+ ),
791
+ backEdge && /* @__PURE__ */ jsx3(
792
+ "span",
793
+ {
794
+ title: visits !== void 0 ? `Loops back \u2014 each node runs at most ${visits} time${visits === 1 ? "" : "s"} per run` : "Loops back to a step that has already run",
795
+ className: `${EDGE_CHIP_CLASS} shrink-0 text-foreground`,
796
+ children: visits !== void 0 ? `\u21BA \u2264${visits}` : "\u21BA"
797
+ }
798
+ )
799
+ ]
800
+ }
801
+ ) })
802
+ ] });
803
+ }
698
804
  var NODE_TYPES = { wfNode: WorkflowNode };
699
- function buildStyledEdges(base, nodeState) {
805
+ var EDGE_TYPES = { [WF_EDGE_TYPE]: WfEdgeRenderer };
806
+ function buildStyledEdges(base, nodeState, maxNodeVisits) {
700
807
  return base.map((e) => {
701
808
  const status = nodeState?.[e.target]?.status;
702
809
  const color = edgeColor(nodeState ? status : void 0);
810
+ const backEdge = e.data?.backEdge === true;
703
811
  return {
704
812
  ...e,
705
- // The active hop flows; everything else is static.
706
- animated: status === "running",
707
- style: { strokeWidth: 1.75, stroke: color },
813
+ // The active hop flows; everything else is static. A cycle-closing edge
814
+ // never flows even into a running target: it is the loop's RETURN path,
815
+ // and animating it would read as the run travelling backwards.
816
+ animated: !backEdge && status === "running",
817
+ style: {
818
+ strokeWidth: 1.75,
819
+ stroke: color,
820
+ // Dashed, so a return path is distinguishable from a forward hop before
821
+ // its badge is read — and at a zoom where the badge isn't legible.
822
+ ...backEdge ? { strokeDasharray: "6 3" } : {}
823
+ },
708
824
  markerEnd: {
709
825
  type: MarkerType.ArrowClosed,
710
826
  width: 15,
711
827
  height: 15,
712
828
  color
713
- }
829
+ },
830
+ ...backEdge && maxNodeVisits !== void 0 && e.data ? { data: { ...e.data, maxNodeVisits } } : {}
714
831
  };
715
832
  });
716
833
  }
@@ -721,28 +838,41 @@ function WorkflowGraph({
721
838
  defaultCompact = true,
722
839
  className,
723
840
  nodeState,
724
- onNodeClick
841
+ edges: declaredEdges,
842
+ maxNodeVisits,
843
+ selectedNodeId,
844
+ onNodeClick,
845
+ onEdgeConnect,
846
+ onEdgeDelete,
847
+ onEdgeClick
725
848
  }) {
849
+ const editable = onEdgeConnect !== void 0;
726
850
  const colorMode = useColorMode();
727
851
  const isPreview = variant === "preview";
728
852
  const [userCompact, setUserCompact] = useState(defaultCompact);
729
853
  const compact = isPreview || userCompact;
730
854
  const [displayCompact, setDisplayCompact] = useState(compact);
731
- const rfRef = useRef(null);
855
+ const rfRef = useRef(
856
+ null
857
+ );
732
858
  const wrapperRef = useRef(null);
733
859
  const hasRunOverlay = nodeState !== void 0;
734
860
  const structural = useMemo(
735
861
  () => buildFlowGraph(yaml, {
736
862
  nodeState: hasRunOverlay ? {} : void 0,
737
863
  direction,
738
- compact
864
+ compact,
865
+ ...declaredEdges ? { edges: declaredEdges } : {}
739
866
  }),
740
- [yaml, hasRunOverlay, direction, compact]
741
- );
742
- const styledEdges = useMemo(
743
- () => buildStyledEdges(structural.edges, nodeState),
744
- [structural.edges, nodeState]
867
+ [yaml, hasRunOverlay, direction, compact, declaredEdges]
745
868
  );
869
+ const styledEdges = useMemo(() => {
870
+ const styled = buildStyledEdges(structural.edges, nodeState, maxNodeVisits);
871
+ return styled.map((e) => ({
872
+ ...e,
873
+ deletable: editable ? isEditableEdge(e) : false
874
+ }));
875
+ }, [structural.edges, nodeState, maxNodeVisits, editable]);
746
876
  const [nodes, setNodes, onNodesChange] = useNodesState(structural.nodes);
747
877
  const [edges, setEdges, onEdgesChange] = useEdgesState(styledEdges);
748
878
  const nodesRef = useRef(nodes);
@@ -853,7 +983,7 @@ function WorkflowGraph({
853
983
  }
854
984
  );
855
985
  }
856
- return /* @__PURE__ */ jsx3(DirectionContext.Provider, { value: direction, children: /* @__PURE__ */ jsx3(DensityContext.Provider, { value: displayCompact, children: /* @__PURE__ */ jsx3("div", { ref: wrapperRef, className: `wf-graph ${className ?? ""}`, children: /* @__PURE__ */ jsxs2(
986
+ return /* @__PURE__ */ jsx3(DirectionContext.Provider, { value: direction, children: /* @__PURE__ */ jsx3(DensityContext.Provider, { value: displayCompact, children: /* @__PURE__ */ jsx3(SelectedNodeContext.Provider, { value: selectedNodeId, children: /* @__PURE__ */ jsx3(ConnectableContext.Provider, { value: editable, children: /* @__PURE__ */ jsx3("div", { ref: wrapperRef, className: `wf-graph ${className ?? ""}`, children: /* @__PURE__ */ jsxs2(
857
987
  ReactFlow,
858
988
  {
859
989
  nodes,
@@ -861,6 +991,7 @@ function WorkflowGraph({
861
991
  onNodesChange,
862
992
  onEdgesChange,
863
993
  nodeTypes: NODE_TYPES,
994
+ edgeTypes: EDGE_TYPES,
864
995
  colorMode,
865
996
  onInit: (inst) => {
866
997
  rfRef.current = inst;
@@ -870,11 +1001,28 @@ function WorkflowGraph({
870
1001
  fitViewOptions: { ...FIT_VIEW, maxZoom: fitZoomCeiling(compact) },
871
1002
  proOptions: { hideAttribution: true },
872
1003
  nodesDraggable: !isPreview,
873
- nodesConnectable: false,
874
- deleteKeyCode: null,
1004
+ nodesConnectable: editable,
1005
+ deleteKeyCode: editable ? ["Backspace", "Delete"] : null,
875
1006
  edgesReconnectable: false,
876
1007
  elementsSelectable: !isPreview,
877
- edgesFocusable: false,
1008
+ edgesFocusable: editable,
1009
+ onConnect: editable ? (connection) => {
1010
+ if (connection.source && connection.target) {
1011
+ onEdgeConnect(connection.source, connection.target);
1012
+ }
1013
+ } : void 0,
1014
+ onEdgesDelete: editable ? (deleted) => {
1015
+ for (const edge of deleted) {
1016
+ if (isEditableEdge(edge)) {
1017
+ onEdgeDelete?.(edge.source, edge.target);
1018
+ }
1019
+ }
1020
+ } : void 0,
1021
+ onEdgeClick: editable ? (_event, edge) => {
1022
+ if (isEditableEdge(edge)) {
1023
+ onEdgeClick?.(edge.source, edge.target);
1024
+ }
1025
+ } : void 0,
878
1026
  zoomOnScroll: false,
879
1027
  zoomOnPinch: true,
880
1028
  zoomOnDoubleClick: !isPreview,
@@ -903,14 +1051,17 @@ function WorkflowGraph({
903
1051
  ] })
904
1052
  ]
905
1053
  }
906
- ) }) }) });
1054
+ ) }) }) }) }) });
907
1055
  }
908
1056
  export {
1057
+ ConnectableContext,
909
1058
  DensityContext,
910
1059
  DirectionContext,
911
1060
  LAYOUT_TRANSITION_MS,
1061
+ SelectedNodeContext,
912
1062
  WorkflowGraph,
913
1063
  WorkflowNode,
914
1064
  buildStyledEdges,
915
- fitZoomCeiling
1065
+ fitZoomCeiling,
1066
+ isEditableEdge
916
1067
  };
package/dist/chat.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  snapHarnessToModel,
20
20
  snapModelToHarness,
21
21
  validateComposerFiles
22
- } from "./chunk-QMY3XVH3.js";
22
+ } from "./chunk-GUULFESL.js";
23
23
  import "./chunk-TK46XFLM.js";
24
24
  import {
25
25
  MENTION_PILL_CLASS
@@ -1561,17 +1561,19 @@ function AgentComposer({
1561
1561
  }
1562
1562
  )
1563
1563
  ] }),
1564
- /* @__PURE__ */ jsx4("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: controls ?? /* @__PURE__ */ jsx4(
1565
- AgentSessionControls,
1566
- {
1567
- context,
1568
- harness,
1569
- profile,
1570
- model,
1571
- reasoning
1572
- }
1573
- ) }),
1574
- trailing && /* @__PURE__ */ jsx4("div", { className: "flex shrink-0 items-center gap-2", children: trailing }),
1564
+ /* @__PURE__ */ jsxs4("div", { className: "flex min-w-0 flex-1 items-center gap-2 overflow-x-auto overscroll-x-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", children: [
1565
+ controls ?? /* @__PURE__ */ jsx4(
1566
+ AgentSessionControls,
1567
+ {
1568
+ context,
1569
+ harness,
1570
+ profile,
1571
+ model,
1572
+ reasoning
1573
+ }
1574
+ ),
1575
+ trailing && /* @__PURE__ */ jsx4("div", { className: "ml-auto flex shrink-0 items-center gap-2", children: trailing })
1576
+ ] }),
1575
1577
  busy && onCancel ? /* @__PURE__ */ jsx4(
1576
1578
  "button",
1577
1579
  {