@tangle-network/sandbox-ui 0.73.0 → 0.75.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.
@@ -15,20 +15,22 @@ function providerLabel(provider) {
15
15
 
16
16
  // src/workflows/model.ts
17
17
  import { parse as parseYaml } from "yaml";
18
- var BRANCH_X = 300;
19
- var NODE_GAP = 44;
20
- var BRANCH_GAP = 24;
21
- var CARD_CHROME = 20;
22
- var TITLE_ROW = 22;
18
+ var NODE_W = 244;
19
+ var RANK_SEP = 76;
20
+ var CROSS_SEP = 22;
21
+ var CARD_CHROME = 18;
22
+ var HEADER_ROW = 28;
23
23
  var TEXT_ROW = 20;
24
- var META_ROW = 44;
25
- var PREVIEW_ROW = 34;
26
- function estimateNodeHeight(data, withRunState) {
27
- let h = CARD_CHROME + TITLE_ROW;
24
+ var META_ROW = 46;
25
+ var OUTPUT_ROW = 44;
26
+ var FOOTER_ROW = 26;
27
+ var COMPACT_NODE_SIZE = { width: 240, height: 64 };
28
+ function nodeHeight(data, withRunState) {
29
+ let h = CARD_CHROME + HEADER_ROW;
28
30
  if (data.subtitle) h += TEXT_ROW;
29
- if (withRunState || data.model) h += META_ROW;
30
- if (withRunState) h += PREVIEW_ROW;
31
31
  if (data.provider) h += TEXT_ROW;
32
+ if (withRunState || data.model) h += META_ROW;
33
+ if (withRunState) h += OUTPUT_ROW + FOOTER_ROW;
32
34
  return h;
33
35
  }
34
36
  function asRecord(v) {
@@ -65,7 +67,6 @@ function describeActionBase(action) {
65
67
  kind,
66
68
  subtitle: str(cfg.template),
67
69
  detail: pickDetail(cfg, ["template", "size", "region"]),
68
- hasBranches: false,
69
70
  isRoot: false,
70
71
  tone: "action"
71
72
  };
@@ -78,7 +79,6 @@ function describeActionBase(action) {
78
79
  path,
79
80
  provider: path?.split(".")[0],
80
81
  detail: pickDetail(cfg, ["path"]),
81
- hasBranches: false,
82
82
  isRoot: false,
83
83
  tone: "action"
84
84
  };
@@ -90,7 +90,6 @@ function describeActionBase(action) {
90
90
  kind,
91
91
  subtitle: url ? urlHost(url) : void 0,
92
92
  detail: pickDetail(cfg, ["url"]),
93
- hasBranches: false,
94
93
  isRoot: false,
95
94
  tone: "action"
96
95
  };
@@ -99,7 +98,9 @@ function describeActionBase(action) {
99
98
  return {
100
99
  title: "Run agent",
101
100
  kind,
102
- subtitle: str(cfg.model) ?? str(cfg.profile) ?? str(cfg.prompt),
101
+ // The prompt says what the agent does; the model is shown as its own
102
+ // chip, so it's not duplicated here.
103
+ subtitle: str(cfg.prompt) ?? str(cfg.profile),
103
104
  model: str(cfg.model),
104
105
  detail: pickDetail(cfg, [
105
106
  "profile",
@@ -108,7 +109,6 @@ function describeActionBase(action) {
108
109
  "size",
109
110
  "prompt"
110
111
  ]),
111
- hasBranches: false,
112
112
  isRoot: false,
113
113
  tone: "action"
114
114
  };
@@ -119,7 +119,6 @@ function describeActionBase(action) {
119
119
  kind,
120
120
  subtitle: `${branches.length} branch${branches.length === 1 ? "" : "es"}`,
121
121
  badge: branches.length > 0 ? `\xD7${branches.length}` : void 0,
122
- hasBranches: branches.length > 0,
123
122
  isRoot: false,
124
123
  tone: "structural"
125
124
  };
@@ -131,7 +130,6 @@ function describeActionBase(action) {
131
130
  subtitle: typeof cfg.items === "string" ? cfg.items : "list",
132
131
  detail: pickDetail(cfg, ["items"]),
133
132
  // Only fans out when a `do` template is actually present.
134
- hasBranches: Boolean(cfg.do),
135
133
  isRoot: false,
136
134
  tone: "structural"
137
135
  };
@@ -139,7 +137,6 @@ function describeActionBase(action) {
139
137
  return {
140
138
  title: kind ? kind : "Action",
141
139
  kind: kind ?? "action",
142
- hasBranches: false,
143
140
  isRoot: false,
144
141
  tone: "action"
145
142
  };
@@ -198,7 +195,6 @@ function describeTrigger(on) {
198
195
  kind: "provider_event",
199
196
  subtitle,
200
197
  provider: connection,
201
- hasBranches: false,
202
198
  isRoot: true,
203
199
  tone: "trigger"
204
200
  },
@@ -214,7 +210,6 @@ function describeTrigger(on) {
214
210
  title: "Schedule",
215
211
  kind: "schedule",
216
212
  subtitle: cron ? tz ? `${cron} (${tz})` : cron : void 0,
217
- hasBranches: false,
218
213
  isRoot: true,
219
214
  tone: "trigger"
220
215
  },
@@ -226,15 +221,58 @@ function describeTrigger(on) {
226
221
  {
227
222
  title: "Trigger",
228
223
  kind: "trigger",
229
- hasBranches: false,
230
224
  isRoot: true,
231
225
  tone: "trigger"
232
226
  },
233
227
  asRecord(kind ? rec[kind] : void 0)
234
228
  );
235
229
  }
230
+ function layoutLayers(nodes, direction) {
231
+ const isLR = direction === "LR";
232
+ const mainSize = (n) => isLR ? n.width : n.height;
233
+ const crossSize = (n) => isLR ? n.height : n.width;
234
+ const byRank = /* @__PURE__ */ new Map();
235
+ for (const n of nodes) {
236
+ const arr = byRank.get(n.rank);
237
+ if (arr) arr.push(n);
238
+ else byRank.set(n.rank, [n]);
239
+ }
240
+ const ranks = [...byRank.keys()].sort((a, b) => a - b);
241
+ const mainStart = /* @__PURE__ */ new Map();
242
+ let cursor = 0;
243
+ for (const r of ranks) {
244
+ mainStart.set(r, cursor);
245
+ cursor += Math.max(...byRank.get(r).map(mainSize)) + RANK_SEP;
246
+ }
247
+ const out = [];
248
+ for (const r of ranks) {
249
+ const layer = byRank.get(r);
250
+ const extent = Math.max(...layer.map(mainSize));
251
+ const span = layer.reduce((s, n) => s + crossSize(n), 0) + CROSS_SEP * (layer.length - 1);
252
+ let cross = -span / 2;
253
+ for (const n of layer) {
254
+ const main = mainStart.get(r) + (extent - mainSize(n)) / 2;
255
+ out.push({
256
+ id: n.id,
257
+ position: isLR ? { x: main, y: cross } : { x: cross, y: main },
258
+ width: n.width,
259
+ height: n.height,
260
+ data: n.data
261
+ });
262
+ cross += crossSize(n) + CROSS_SEP;
263
+ }
264
+ }
265
+ const minX = Math.min(...out.map((n) => n.position.x));
266
+ const minY = Math.min(...out.map((n) => n.position.y));
267
+ for (const n of out) {
268
+ n.position = { x: n.position.x - minX, y: n.position.y - minY };
269
+ }
270
+ return out;
271
+ }
236
272
  function buildWorkflowGraph(yaml, options) {
237
273
  const reserveRunState = options?.reserveRunState ?? false;
274
+ const direction = options?.direction ?? "LR";
275
+ const measure = options?.measure;
238
276
  if (!yaml || yaml.trim() === "") {
239
277
  return { nodes: [], edges: [], error: "No definition" };
240
278
  }
@@ -249,61 +287,58 @@ function buildWorkflowGraph(yaml, options) {
249
287
  if (!def.on && actions.length === 0) {
250
288
  return { nodes: [], edges: [], error: "Empty workflow" };
251
289
  }
252
- const nodes = [];
290
+ const logical = [];
253
291
  const edges = [];
254
- let y = 0;
255
- let prevId = null;
256
- const addEdge = (source, target, branch) => {
257
- edges.push({
258
- id: `${source}->${target}`,
259
- source,
260
- target,
261
- sourceHandle: branch ? "branch" : "out"
262
- });
292
+ const addNode = (id, data, rank2, withRunState) => {
293
+ const dims = measure ? measure(data, withRunState) : { width: NODE_W, height: nodeHeight(data, withRunState) };
294
+ logical.push({ id, data, width: dims.width, height: dims.height, rank: rank2 });
295
+ };
296
+ const addEdge = (source, target, kind) => {
297
+ edges.push({ id: `${source}->${target}`, source, target, kind });
263
298
  };
299
+ let rank = 0;
300
+ let prevExits = [];
301
+ const branchLeafIds = /* @__PURE__ */ new Set();
264
302
  if (def.on) {
265
- const data = describeTrigger(def.on);
266
- nodes.push({ id: "trigger", position: { x: 0, y }, data });
267
- prevId = "trigger";
268
- y += estimateNodeHeight(data, false) + NODE_GAP;
303
+ addNode("trigger", describeTrigger(def.on), rank, false);
304
+ prevExits = ["trigger"];
305
+ rank += 1;
269
306
  }
270
307
  actions.forEach((action, i) => {
271
308
  const id = `a${i}`;
272
309
  const data = describeAction(action);
273
- nodes.push({ id, position: { x: 0, y }, data });
274
- if (prevId) addEdge(prevId, id, false);
275
- prevId = id;
276
- const spineHeight = estimateNodeHeight(data, reserveRunState);
310
+ if (i === 0 && !def.on) data.isRoot = true;
311
+ addNode(id, data, rank, reserveRunState);
312
+ for (const from of prevExits) {
313
+ addEdge(from, id, branchLeafIds.has(from) ? "join" : "spine");
314
+ }
277
315
  const rec = asRecord(action);
278
316
  const children = "parallel" in rec ? Array.isArray(asRecord(rec.parallel).branches) ? asRecord(rec.parallel).branches : [] : "foreach" in rec ? (
279
317
  // A foreach with no `do` template has no child to render.
280
318
  [asRecord(rec.foreach).do].filter(Boolean)
281
319
  ) : [];
282
320
  if (children.length > 0) {
283
- let by = y;
284
- let branchStackHeight = 0;
321
+ const branchRank = rank + 1;
322
+ const branchIds = [];
285
323
  children.forEach((child, j) => {
286
324
  const cid = `${id}-b${j}`;
287
- const childData = describeAction(child);
288
- nodes.push({
289
- id: cid,
290
- position: { x: BRANCH_X, y: by },
291
- data: childData
292
- });
293
- addEdge(id, cid, true);
294
- const childHeight = estimateNodeHeight(childData, reserveRunState);
295
- by += childHeight + BRANCH_GAP;
296
- branchStackHeight += childHeight + BRANCH_GAP;
325
+ addNode(cid, describeAction(child), branchRank, reserveRunState);
326
+ addEdge(id, cid, "fork");
327
+ branchIds.push(cid);
328
+ branchLeafIds.add(cid);
297
329
  });
298
- y += Math.max(spineHeight, branchStackHeight) + NODE_GAP;
330
+ prevExits = branchIds;
331
+ rank = branchRank + 1;
299
332
  } else {
300
- y += spineHeight + NODE_GAP;
333
+ prevExits = [id];
334
+ rank += 1;
301
335
  }
302
336
  });
303
- return { nodes, edges, error: null };
337
+ return { nodes: layoutLayers(logical, direction), edges, error: null };
304
338
  }
305
339
 
306
340
  export {
307
341
  providerLabel,
342
+ COMPACT_NODE_SIZE,
308
343
  buildWorkflowGraph
309
344
  };
package/dist/globals.css CHANGED
@@ -75,6 +75,7 @@
75
75
  --tracking-wider: 0.05em;
76
76
  --tracking-widest: 0.1em;
77
77
  --leading-tight: 1.25;
78
+ --leading-snug: 1.375;
78
79
  --leading-relaxed: 1.625;
79
80
  --radius-sm: 0.25rem;
80
81
  --radius-md: 0.375rem;
@@ -542,6 +543,9 @@
542
543
  .mt-6 {
543
544
  margin-top: calc(var(--spacing) * 6);
544
545
  }
546
+ .mt-auto {
547
+ margin-top: auto;
548
+ }
545
549
  .mr-1 {
546
550
  margin-right: calc(var(--spacing) * 1);
547
551
  }
@@ -626,6 +630,12 @@
626
630
  -webkit-box-orient: vertical;
627
631
  -webkit-line-clamp: 2;
628
632
  }
633
+ .line-clamp-3 {
634
+ overflow: hidden;
635
+ display: -webkit-box;
636
+ -webkit-box-orient: vertical;
637
+ -webkit-line-clamp: 3;
638
+ }
629
639
  .block {
630
640
  display: block;
631
641
  }
@@ -697,6 +707,9 @@
697
707
  .\!h-2 {
698
708
  height: calc(var(--spacing) * 2) !important;
699
709
  }
710
+ .h-0\.5 {
711
+ height: calc(var(--spacing) * 0.5);
712
+ }
700
713
  .h-1 {
701
714
  height: calc(var(--spacing) * 1);
702
715
  }
@@ -772,12 +785,24 @@
772
785
  .h-\[18px\] {
773
786
  height: 18px;
774
787
  }
788
+ .h-\[24rem\] {
789
+ height: 24rem;
790
+ }
791
+ .h-\[28rem\] {
792
+ height: 28rem;
793
+ }
794
+ .h-\[32rem\] {
795
+ height: 32rem;
796
+ }
775
797
  .h-\[70vh\] {
776
798
  height: 70vh;
777
799
  }
778
800
  .h-\[72\%\] {
779
801
  height: 72%;
780
802
  }
803
+ .h-\[220px\] {
804
+ height: 220px;
805
+ }
781
806
  .h-\[380px\] {
782
807
  height: 380px;
783
808
  }
@@ -1072,9 +1097,6 @@
1072
1097
  .w-\[88\%\] {
1073
1098
  width: 88%;
1074
1099
  }
1075
- .w-\[240px\] {
1076
- width: 240px;
1077
- }
1078
1100
  .w-\[268px\] {
1079
1101
  width: 268px;
1080
1102
  }
@@ -1114,6 +1136,9 @@
1114
1136
  .w-\[900px\] {
1115
1137
  width: 900px;
1116
1138
  }
1139
+ .w-\[1000px\] {
1140
+ width: 1000px;
1141
+ }
1117
1142
  .w-\[min\(88vw\,24rem\)\] {
1118
1143
  width: min(88vw, 24rem);
1119
1144
  }
@@ -1171,6 +1196,9 @@
1171
1196
  .max-w-\[14rem\] {
1172
1197
  max-width: 14rem;
1173
1198
  }
1199
+ .max-w-\[45\%\] {
1200
+ max-width: 45%;
1201
+ }
1174
1202
  .max-w-\[75\%\] {
1175
1203
  max-width: 75%;
1176
1204
  }
@@ -1907,6 +1935,12 @@
1907
1935
  .border-border {
1908
1936
  border-color: var(--color-border);
1909
1937
  }
1938
+ .border-border\/60 {
1939
+ border-color: color-mix(in srgb, hsl(var(--border)) 60%, transparent);
1940
+ @supports (color: color-mix(in lab, red, red)) {
1941
+ border-color: color-mix(in oklab, var(--color-border) 60%, transparent);
1942
+ }
1943
+ }
1910
1944
  .border-destructive\/20 {
1911
1945
  border-color: color-mix(in srgb, hsl(var(--destructive)) 20%, transparent);
1912
1946
  @supports (color: color-mix(in lab, red, red)) {
@@ -1994,12 +2028,6 @@
1994
2028
  border-color: color-mix(in oklab, var(--color-primary) 50%, transparent);
1995
2029
  }
1996
2030
  }
1997
- .border-primary\/60 {
1998
- border-color: color-mix(in srgb, hsl(var(--primary)) 60%, transparent);
1999
- @supports (color: color-mix(in lab, red, red)) {
2000
- border-color: color-mix(in oklab, var(--color-primary) 60%, transparent);
2001
- }
2002
- }
2003
2031
  .border-red-500 {
2004
2032
  border-color: var(--color-red-500);
2005
2033
  }
@@ -2952,6 +2980,10 @@
2952
2980
  --tw-leading: var(--leading-relaxed);
2953
2981
  line-height: var(--leading-relaxed);
2954
2982
  }
2983
+ .leading-snug {
2984
+ --tw-leading: var(--leading-snug);
2985
+ line-height: var(--leading-snug);
2986
+ }
2955
2987
  .leading-tight {
2956
2988
  --tw-leading: var(--leading-tight);
2957
2989
  line-height: var(--leading-tight);
@@ -3277,6 +3309,12 @@
3277
3309
  .text-red-400 {
3278
3310
  color: var(--color-red-400);
3279
3311
  }
3312
+ .text-red-400\/80 {
3313
+ color: color-mix(in srgb, oklch(70.4% 0.191 22.216) 80%, transparent);
3314
+ @supports (color: color-mix(in lab, red, red)) {
3315
+ color: color-mix(in oklab, var(--color-red-400) 80%, transparent);
3316
+ }
3317
+ }
3280
3318
  .text-secondary-foreground {
3281
3319
  color: var(--color-secondary-foreground);
3282
3320
  }
@@ -5116,6 +5154,11 @@
5116
5154
  grid-column: span 8 / span 8;
5117
5155
  }
5118
5156
  }
5157
+ .xl\:grid-cols-2 {
5158
+ @media (width >= 80rem) {
5159
+ grid-template-columns: repeat(2, minmax(0, 1fr));
5160
+ }
5161
+ }
5119
5162
  .xl\:grid-cols-4 {
5120
5163
  @media (width >= 80rem) {
5121
5164
  grid-template-columns: repeat(4, minmax(0, 1fr));
package/dist/styles.css CHANGED
@@ -75,6 +75,7 @@
75
75
  --tracking-wider: 0.05em;
76
76
  --tracking-widest: 0.1em;
77
77
  --leading-tight: 1.25;
78
+ --leading-snug: 1.375;
78
79
  --leading-relaxed: 1.625;
79
80
  --radius-sm: 0.25rem;
80
81
  --radius-md: 0.375rem;
@@ -542,6 +543,9 @@
542
543
  .mt-6 {
543
544
  margin-top: calc(var(--spacing) * 6);
544
545
  }
546
+ .mt-auto {
547
+ margin-top: auto;
548
+ }
545
549
  .mr-1 {
546
550
  margin-right: calc(var(--spacing) * 1);
547
551
  }
@@ -626,6 +630,12 @@
626
630
  -webkit-box-orient: vertical;
627
631
  -webkit-line-clamp: 2;
628
632
  }
633
+ .line-clamp-3 {
634
+ overflow: hidden;
635
+ display: -webkit-box;
636
+ -webkit-box-orient: vertical;
637
+ -webkit-line-clamp: 3;
638
+ }
629
639
  .block {
630
640
  display: block;
631
641
  }
@@ -697,6 +707,9 @@
697
707
  .\!h-2 {
698
708
  height: calc(var(--spacing) * 2) !important;
699
709
  }
710
+ .h-0\.5 {
711
+ height: calc(var(--spacing) * 0.5);
712
+ }
700
713
  .h-1 {
701
714
  height: calc(var(--spacing) * 1);
702
715
  }
@@ -772,12 +785,24 @@
772
785
  .h-\[18px\] {
773
786
  height: 18px;
774
787
  }
788
+ .h-\[24rem\] {
789
+ height: 24rem;
790
+ }
791
+ .h-\[28rem\] {
792
+ height: 28rem;
793
+ }
794
+ .h-\[32rem\] {
795
+ height: 32rem;
796
+ }
775
797
  .h-\[70vh\] {
776
798
  height: 70vh;
777
799
  }
778
800
  .h-\[72\%\] {
779
801
  height: 72%;
780
802
  }
803
+ .h-\[220px\] {
804
+ height: 220px;
805
+ }
781
806
  .h-\[380px\] {
782
807
  height: 380px;
783
808
  }
@@ -1072,9 +1097,6 @@
1072
1097
  .w-\[88\%\] {
1073
1098
  width: 88%;
1074
1099
  }
1075
- .w-\[240px\] {
1076
- width: 240px;
1077
- }
1078
1100
  .w-\[268px\] {
1079
1101
  width: 268px;
1080
1102
  }
@@ -1114,6 +1136,9 @@
1114
1136
  .w-\[900px\] {
1115
1137
  width: 900px;
1116
1138
  }
1139
+ .w-\[1000px\] {
1140
+ width: 1000px;
1141
+ }
1117
1142
  .w-\[min\(88vw\,24rem\)\] {
1118
1143
  width: min(88vw, 24rem);
1119
1144
  }
@@ -1171,6 +1196,9 @@
1171
1196
  .max-w-\[14rem\] {
1172
1197
  max-width: 14rem;
1173
1198
  }
1199
+ .max-w-\[45\%\] {
1200
+ max-width: 45%;
1201
+ }
1174
1202
  .max-w-\[75\%\] {
1175
1203
  max-width: 75%;
1176
1204
  }
@@ -1907,6 +1935,12 @@
1907
1935
  .border-border {
1908
1936
  border-color: var(--color-border);
1909
1937
  }
1938
+ .border-border\/60 {
1939
+ border-color: color-mix(in srgb, hsl(var(--border)) 60%, transparent);
1940
+ @supports (color: color-mix(in lab, red, red)) {
1941
+ border-color: color-mix(in oklab, var(--color-border) 60%, transparent);
1942
+ }
1943
+ }
1910
1944
  .border-destructive\/20 {
1911
1945
  border-color: color-mix(in srgb, hsl(var(--destructive)) 20%, transparent);
1912
1946
  @supports (color: color-mix(in lab, red, red)) {
@@ -1994,12 +2028,6 @@
1994
2028
  border-color: color-mix(in oklab, var(--color-primary) 50%, transparent);
1995
2029
  }
1996
2030
  }
1997
- .border-primary\/60 {
1998
- border-color: color-mix(in srgb, hsl(var(--primary)) 60%, transparent);
1999
- @supports (color: color-mix(in lab, red, red)) {
2000
- border-color: color-mix(in oklab, var(--color-primary) 60%, transparent);
2001
- }
2002
- }
2003
2031
  .border-red-500 {
2004
2032
  border-color: var(--color-red-500);
2005
2033
  }
@@ -2952,6 +2980,10 @@
2952
2980
  --tw-leading: var(--leading-relaxed);
2953
2981
  line-height: var(--leading-relaxed);
2954
2982
  }
2983
+ .leading-snug {
2984
+ --tw-leading: var(--leading-snug);
2985
+ line-height: var(--leading-snug);
2986
+ }
2955
2987
  .leading-tight {
2956
2988
  --tw-leading: var(--leading-tight);
2957
2989
  line-height: var(--leading-tight);
@@ -3277,6 +3309,12 @@
3277
3309
  .text-red-400 {
3278
3310
  color: var(--color-red-400);
3279
3311
  }
3312
+ .text-red-400\/80 {
3313
+ color: color-mix(in srgb, oklch(70.4% 0.191 22.216) 80%, transparent);
3314
+ @supports (color: color-mix(in lab, red, red)) {
3315
+ color: color-mix(in oklab, var(--color-red-400) 80%, transparent);
3316
+ }
3317
+ }
3280
3318
  .text-secondary-foreground {
3281
3319
  color: var(--color-secondary-foreground);
3282
3320
  }
@@ -5116,6 +5154,11 @@
5116
5154
  grid-column: span 8 / span 8;
5117
5155
  }
5118
5156
  }
5157
+ .xl\:grid-cols-2 {
5158
+ @media (width >= 80rem) {
5159
+ grid-template-columns: repeat(2, minmax(0, 1fr));
5160
+ }
5161
+ }
5119
5162
  .xl\:grid-cols-4 {
5120
5163
  @media (width >= 80rem) {
5121
5164
  grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -40,6 +40,9 @@ interface WfNodeState {
40
40
  /** Input/output token usage, when known. */
41
41
  inputTokens?: number;
42
42
  outputTokens?: number;
43
+ /** Agent iteration count (agent.run only) — the number of reasoning/tool
44
+ * rounds the agent has taken, surfaced on the node's progress strip. */
45
+ rounds?: number;
43
46
  }
44
47
  interface WfNodeData extends Record<string, unknown> {
45
48
  /** Headline for the node, e.g. "Run agent". */
@@ -70,8 +73,6 @@ interface WfNodeData extends Record<string, unknown> {
70
73
  provider?: string;
71
74
  /** Small corner tag, e.g. "×3" for a parallel fan-out. */
72
75
  badge?: string;
73
- /** Whether this node fans out to branch leaves (renders a right handle). */
74
- hasBranches: boolean;
75
76
  /** Whether this node is the spine root (no incoming/target handle). */
76
77
  isRoot: boolean;
77
78
  tone: WfNodeTone;
@@ -84,14 +85,21 @@ interface WfNode {
84
85
  x: number;
85
86
  y: number;
86
87
  };
88
+ /** Authoritative rendered size (px). The card renders at exactly this box
89
+ * (`h-full`/`w-full`) so the layout — which spaces nodes by these dims — is
90
+ * collision-free by construction, with no measure-then-reflow pass. */
91
+ width: number;
92
+ height: number;
87
93
  data: WfNodeData;
88
94
  }
95
+ type WfEdgeKind = "spine" | "fork" | "join";
89
96
  interface WfEdge {
90
97
  id: string;
91
98
  source: string;
92
99
  target: string;
93
- /** "out" for spine edges (bottom handle), "branch" for fan-out (right). */
94
- sourceHandle: "out" | "branch";
100
+ /** Spine = trigger→action→action; fork = fan-out into a branch leaf; join =
101
+ * a branch leaf reconverging onto the next spine node. Drives edge styling. */
102
+ kind: WfEdgeKind;
95
103
  }
96
104
  interface WfGraph {
97
105
  nodes: WfNode[];
@@ -99,21 +107,33 @@ interface WfGraph {
99
107
  /** Set when the YAML couldn't be parsed into a renderable graph. */
100
108
  error: string | null;
101
109
  }
110
+ /** Main-axis flow direction: "LR" (left-to-right, default) suits the wide/short
111
+ * run-detail panel; "TB" (top-to-bottom) suits a narrow column. */
112
+ type WfDirection = "LR" | "TB";
102
113
  /** Options for {@link buildWorkflowGraph}. */
103
114
  interface BuildWorkflowGraphOptions {
104
115
  /**
105
- * Reserve vertical space for the rows live run state adds to a spine node (the
106
- * meta-chip row + the output/error preview), so the RUN view's spine never
107
- * overlaps once a node starts or terminates. The static/preview layout (no run
108
- * overlay) leaves this off to stay compact. Defaults to `false`.
116
+ * Reserve space for the rows live run state adds to a node (the meta-chip row +
117
+ * the output/error preview), so the RUN view never overlaps once a node starts
118
+ * or terminates. The static/preview layout (no run overlay) leaves this off to
119
+ * stay compact. Defaults to `false`.
109
120
  */
110
121
  reserveRunState?: boolean;
122
+ /** Flow direction. Defaults to "LR". */
123
+ direction?: WfDirection;
124
+ /** Override the node dimensions the layout spaces by (and the card renders at).
125
+ * Defaults to a fixed width + {@link nodeHeight}. A caller with a different node
126
+ * design supplies its own sizing so the layout stays collision-free. */
127
+ measure?: (data: WfNodeData, withRunState: boolean) => {
128
+ width: number;
129
+ height: number;
130
+ };
111
131
  }
112
132
  /** Build a positioned graph from a workflow YAML string. Never throws —
113
133
  * malformed YAML or an empty definition returns an `error` the UI can fall
114
- * back on (e.g. show the raw YAML while authoring). Pass
115
- * `{ reserveRunState: true }` for the run view so the spine leaves room for the
116
- * rows live run state adds (see {@link estimateNodeHeight}). */
134
+ * back on (e.g. show the raw YAML while authoring). `reserveRunState` leaves
135
+ * room for the rows live run state adds (see {@link nodeHeight}); `direction`
136
+ * picks the flow axis (default "LR"). */
117
137
  declare function buildWorkflowGraph(yaml: string, options?: BuildWorkflowGraphOptions): WfGraph;
118
138
 
119
139
  interface WorkflowGraphProps {
@@ -121,6 +141,13 @@ interface WorkflowGraphProps {
121
141
  yaml: string;
122
142
  /** "full" = interactive (pan/zoom/drag + controls); "preview" = static fit. */
123
143
  variant?: "full" | "preview";
144
+ /** Flow direction: "LR" (default) reads left-to-right — best for the wide/short
145
+ * run-detail panel; "TB" is a vertical column. */
146
+ direction?: WfDirection;
147
+ /** Start collapsed (compact icon tiles) rather than expanded. The full variant
148
+ * exposes a toggle to switch densities; the preview variant is always compact.
149
+ * Defaults to `false` (expanded). */
150
+ defaultCompact?: boolean;
124
151
  /** Sizing for the wrapper; the caller controls height. */
125
152
  className?: string;
126
153
  /**