@tangle-network/sandbox-ui 0.75.0 → 0.77.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.
@@ -0,0 +1,606 @@
1
+ // src/workflows/format.ts
2
+ function fmtDuration(ms) {
3
+ if (ms === void 0 || !Number.isFinite(ms) || ms < 0) return void 0;
4
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
5
+ const s = ms / 1e3;
6
+ if (s < 60) return `${s.toFixed(1)}s`;
7
+ const totalSeconds = Math.round(s);
8
+ return `${Math.floor(totalSeconds / 60)}m${totalSeconds % 60}s`;
9
+ }
10
+ function fmtCost(usd) {
11
+ if (usd === void 0 || !Number.isFinite(usd) || usd < 0) return void 0;
12
+ if (usd === 0) return "$0";
13
+ return usd < 0.01 ? `$${usd.toFixed(4)}` : `$${usd.toFixed(2)}`;
14
+ }
15
+ function clampPreview(text, max = 200) {
16
+ if (text.length <= max) return text;
17
+ const lastCode = text.charCodeAt(max - 1);
18
+ const end = lastCode >= 55296 && lastCode <= 56319 ? max - 1 : max;
19
+ return `${text.slice(0, end)}\u2026`;
20
+ }
21
+ function fmtTokens(input, output) {
22
+ const valid = (n) => n !== void 0 && Number.isFinite(n) && n >= 0 ? n : void 0;
23
+ const i = valid(input);
24
+ const o = valid(output);
25
+ if (i === void 0 && o === void 0) return void 0;
26
+ return `${i ?? 0}/${o ?? 0} tok`;
27
+ }
28
+
29
+ // src/workflows/naming.ts
30
+ var ACRONYMS = /* @__PURE__ */ new Set([
31
+ "ai",
32
+ "api",
33
+ "cd",
34
+ "ci",
35
+ "cli",
36
+ "cpu",
37
+ "css",
38
+ "csv",
39
+ "db",
40
+ "dns",
41
+ "gpu",
42
+ "html",
43
+ "http",
44
+ "https",
45
+ "id",
46
+ "io",
47
+ "ip",
48
+ "json",
49
+ "llm",
50
+ "mcp",
51
+ "ml",
52
+ "pr",
53
+ "qa",
54
+ "sdk",
55
+ "sla",
56
+ "sql",
57
+ "ssh",
58
+ "ui",
59
+ "url",
60
+ "uuid",
61
+ "vm",
62
+ "yaml"
63
+ ]);
64
+ function words(id) {
65
+ return id.replace(/[._\-/]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").trim().split(/\s+/).filter(Boolean);
66
+ }
67
+ function humanizeIdentifier(id, lead = "sentence") {
68
+ const parts = words(id);
69
+ if (parts.length === 0) return id;
70
+ return parts.map((word, i) => {
71
+ const lower = word.toLowerCase();
72
+ if (ACRONYMS.has(lower)) return lower.toUpperCase();
73
+ const capitalized = lower.charAt(0).toUpperCase() + lower.slice(1);
74
+ if (lead === "title") return capitalized;
75
+ if (i > 0) return lower;
76
+ return lead === "lower" ? lower : capitalized;
77
+ }).join(" ");
78
+ }
79
+ function shortModel(model) {
80
+ const slash = model.lastIndexOf("/");
81
+ return slash === -1 ? model : model.slice(slash + 1);
82
+ }
83
+ var ACTION_PATH = /^[a-z0-9_-]+(\.[a-z0-9_-]+)+$/i;
84
+ function parseActionPath(path) {
85
+ if (!ACTION_PATH.test(path.trim())) return null;
86
+ const segments = path.trim().split(".");
87
+ const [provider, ...rest] = segments;
88
+ const operation = rest[rest.length - 1];
89
+ const resource = rest.length > 1 ? rest.slice(0, -1).join(".") : void 0;
90
+ return { provider, operation, resource };
91
+ }
92
+ function actionPathLabel(path) {
93
+ const parts = parseActionPath(path);
94
+ if (!parts) return path;
95
+ const operation = humanizeIdentifier(parts.operation, "lower");
96
+ return parts.resource ? `${operation}: ${parts.resource}` : operation;
97
+ }
98
+
99
+ // src/workflows/cron.ts
100
+ var WEEKDAY_NAMES = [
101
+ "Sunday",
102
+ "Monday",
103
+ "Tuesday",
104
+ "Wednesday",
105
+ "Thursday",
106
+ "Friday",
107
+ "Saturday"
108
+ ];
109
+ function evenStep(field, period) {
110
+ const m = /^\*\/(\d+)$/.exec(field);
111
+ if (!m) return null;
112
+ const n = Number.parseInt(m[1], 10);
113
+ if (n < 1 || n >= period || period % n !== 0) return null;
114
+ return n;
115
+ }
116
+ function plainInt(field) {
117
+ if (!/^\d+$/.test(field)) return null;
118
+ return Number.parseInt(field, 10);
119
+ }
120
+ function timeOfDay(minute, hour) {
121
+ const m = plainInt(minute);
122
+ const h = plainInt(hour);
123
+ if (m === null || h === null || m > 59 || h > 23) return null;
124
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
125
+ }
126
+ function weekdays(field) {
127
+ if (field === "1-5") return "Weekdays";
128
+ if (field === "0,6" || field === "6,0" || field === "6-7") return "Weekends";
129
+ const dayName = (value) => {
130
+ if (value < 0 || value > 7) return null;
131
+ return WEEKDAY_NAMES[value === 7 ? 0 : value];
132
+ };
133
+ const single = plainInt(field);
134
+ if (single !== null) {
135
+ const name = dayName(single);
136
+ return name ? `${name}s` : null;
137
+ }
138
+ if (/^\d+(,\d+)+$/.test(field)) {
139
+ const names = field.split(",").map((d) => dayName(Number.parseInt(d, 10)));
140
+ if (names.some((n) => n === null)) return null;
141
+ const unique = [...new Set(names)];
142
+ return unique.map((n) => n.slice(0, 3)).join(", ");
143
+ }
144
+ return null;
145
+ }
146
+ function ordinal(day) {
147
+ const tens = day % 100;
148
+ if (tens >= 11 && tens <= 13) return `${day}th`;
149
+ switch (day % 10) {
150
+ case 1:
151
+ return `${day}st`;
152
+ case 2:
153
+ return `${day}nd`;
154
+ case 3:
155
+ return `${day}rd`;
156
+ default:
157
+ return `${day}th`;
158
+ }
159
+ }
160
+ function describeCron(expression) {
161
+ const raw = expression.trim();
162
+ const fields = raw.split(/\s+/);
163
+ if (fields.length !== 5) return raw;
164
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = fields;
165
+ const anyDayOfMonth = dayOfMonth === "*";
166
+ const anyDayOfWeek = dayOfWeek === "*";
167
+ const anyMonth = month === "*";
168
+ if (!anyMonth) return raw;
169
+ if (!anyDayOfMonth && !anyDayOfWeek) return raw;
170
+ const everyDay = anyDayOfMonth && anyDayOfWeek;
171
+ if (everyDay) {
172
+ if (minute === "*" && hour === "*") return "Every minute";
173
+ const everyNMinutes = evenStep(minute, 60);
174
+ if (everyNMinutes !== null && hour === "*") {
175
+ return everyNMinutes === 1 ? "Every minute" : `Every ${everyNMinutes} minutes`;
176
+ }
177
+ const everyNHours = evenStep(hour, 24);
178
+ const atMinute = plainInt(minute);
179
+ const pastTheHour = atMinute !== null && atMinute <= 59 ? `:${String(atMinute).padStart(2, "0")}` : null;
180
+ if (everyNHours !== null && pastTheHour) {
181
+ return everyNHours === 1 ? `Hourly at ${pastTheHour}` : `Every ${everyNHours} hours at ${pastTheHour}`;
182
+ }
183
+ if (hour === "*" && pastTheHour) return `Hourly at ${pastTheHour}`;
184
+ const time2 = timeOfDay(minute, hour);
185
+ return time2 ? `Daily at ${time2}` : raw;
186
+ }
187
+ const time = timeOfDay(minute, hour);
188
+ if (!time) return raw;
189
+ if (!anyDayOfWeek) {
190
+ const days = weekdays(dayOfWeek);
191
+ return days ? `${days} at ${time}` : raw;
192
+ }
193
+ const day = plainInt(dayOfMonth);
194
+ if (day !== null && day >= 1 && day <= 31) {
195
+ return `Monthly on the ${ordinal(day)} at ${time}`;
196
+ }
197
+ return raw;
198
+ }
199
+
200
+ // src/workflows/provider-label.ts
201
+ var PROVIDER_LABELS = {
202
+ github: "GitHub",
203
+ gitlab: "GitLab",
204
+ slack: "Slack",
205
+ stripe: "Stripe",
206
+ notion: "Notion",
207
+ linear: "Linear",
208
+ discord: "Discord"
209
+ };
210
+ function providerLabel(provider) {
211
+ const key = provider.toLowerCase();
212
+ return PROVIDER_LABELS[key] ?? humanizeIdentifier(provider, "title");
213
+ }
214
+
215
+ // src/workflows/model.ts
216
+ import { parse as parseYaml } from "yaml";
217
+ var NODE_W = 292;
218
+ var RANK_SEP = 72;
219
+ var CROSS_SEP = 24;
220
+ var CARD_CHROME = 20;
221
+ var HEADER_ROW = 34;
222
+ var DESCRIPTION_ROW = 40;
223
+ var META_ROW = 27;
224
+ var OUTPUT_ROW = 72;
225
+ var FOOTER_ROW = 28;
226
+ var COMPACT_TILE = 76;
227
+ var COMPACT_GAP = 8;
228
+ var COMPACT_LABEL_W = 152;
229
+ var COMPACT_NODE_SIZE = {
230
+ width: 168,
231
+ height: COMPACT_TILE + COMPACT_GAP + 34
232
+ };
233
+ var COMPACT_NODE_SIZE_RUN = {
234
+ width: 168,
235
+ height: COMPACT_TILE + COMPACT_GAP + 34 + 15
236
+ };
237
+ var COMPACT_NODE_SIZE_TB = {
238
+ width: COMPACT_TILE + COMPACT_GAP + COMPACT_LABEL_W,
239
+ height: COMPACT_TILE
240
+ };
241
+ function nodeHeight(data, withRunState) {
242
+ let h = CARD_CHROME + HEADER_ROW;
243
+ if (data.description) h += DESCRIPTION_ROW;
244
+ if (withRunState) h += META_ROW + OUTPUT_ROW + FOOTER_ROW;
245
+ return h;
246
+ }
247
+ function geometry(compact, runMode, direction) {
248
+ if (compact) {
249
+ if (direction === "TB") {
250
+ return { size: COMPACT_NODE_SIZE_TB, rankSep: 34, crossSep: 24 };
251
+ }
252
+ return {
253
+ size: runMode ? COMPACT_NODE_SIZE_RUN : COMPACT_NODE_SIZE,
254
+ rankSep: 20,
255
+ crossSep: 20
256
+ };
257
+ }
258
+ return { size: void 0, rankSep: RANK_SEP, crossSep: CROSS_SEP };
259
+ }
260
+ function asRecord(v) {
261
+ return v && typeof v === "object" && !Array.isArray(v) ? v : {};
262
+ }
263
+ function str(v) {
264
+ return typeof v === "string" && v.trim() !== "" ? v : void 0;
265
+ }
266
+ function describeText(v) {
267
+ const text = str(v);
268
+ return text ? clampPreview(text.trim(), 220) : void 0;
269
+ }
270
+ function urlHost(url) {
271
+ try {
272
+ return new URL(url).host;
273
+ } catch {
274
+ return url;
275
+ }
276
+ }
277
+ var CONTROL_FLOW_KEYS = ["if", "retry", "onError"];
278
+ var CONTROL_FLOW = new Set(CONTROL_FLOW_KEYS);
279
+ function actionKind(rec) {
280
+ return Object.keys(rec).find((k) => !CONTROL_FLOW.has(k));
281
+ }
282
+ function agentTitle(profile) {
283
+ const named = str(profile);
284
+ return named ? humanizeIdentifier(named) : "AI Agent";
285
+ }
286
+ function describeActionBase(action) {
287
+ const rec = asRecord(action);
288
+ const kind = actionKind(rec);
289
+ const cfg = asRecord(kind ? rec[kind] : void 0);
290
+ switch (kind) {
291
+ case "sandbox.spawn":
292
+ return {
293
+ title: "Sandbox",
294
+ kind,
295
+ subtitle: str(cfg.template) ?? "Provision",
296
+ description: describeText(cfg.prompt),
297
+ isRoot: false,
298
+ tone: "action"
299
+ };
300
+ case "integration.invoke": {
301
+ const path = str(cfg.path);
302
+ const parts = path ? parseActionPath(path) : null;
303
+ return {
304
+ // The provider IS the node's identity, exactly as in n8n — its logo and
305
+ // its name. What it does with it goes in the subtitle.
306
+ title: parts ? providerLabel(parts.provider) : "Integration",
307
+ kind,
308
+ subtitle: path ? actionPathLabel(path) : void 0,
309
+ path,
310
+ provider: parts?.provider,
311
+ isRoot: false,
312
+ tone: "action"
313
+ };
314
+ }
315
+ case "notify": {
316
+ const url = str(cfg.url);
317
+ return {
318
+ title: "Notify",
319
+ kind,
320
+ subtitle: url ? urlHost(url) : void 0,
321
+ description: describeText(url),
322
+ isRoot: false,
323
+ tone: "action"
324
+ };
325
+ }
326
+ case "agent.run": {
327
+ const model = str(cfg.model);
328
+ return {
329
+ title: agentTitle(cfg.profile),
330
+ kind,
331
+ // The model is the agent's defining trait, so it names the step; the
332
+ // prompt — the long part — is the description the expanded card clamps.
333
+ subtitle: model ? shortModel(model) : "Agent",
334
+ description: describeText(cfg.prompt),
335
+ model,
336
+ isRoot: false,
337
+ tone: "action"
338
+ };
339
+ }
340
+ case "parallel": {
341
+ const branches = Array.isArray(cfg.branches) ? cfg.branches : [];
342
+ return {
343
+ title: "Parallel",
344
+ kind,
345
+ subtitle: `${branches.length} branch${branches.length === 1 ? "" : "es"}`,
346
+ badge: branches.length > 0 ? `\xD7${branches.length}` : void 0,
347
+ isRoot: false,
348
+ tone: "structural"
349
+ };
350
+ }
351
+ case "foreach":
352
+ return {
353
+ title: "For each",
354
+ kind,
355
+ subtitle: "Repeat per item",
356
+ description: typeof cfg.items === "string" ? cfg.items : "Literal list",
357
+ // Only fans out when a `do` template is actually present.
358
+ isRoot: false,
359
+ tone: "structural"
360
+ };
361
+ case "decision": {
362
+ const options = Array.isArray(cfg.options) ? cfg.options.filter((o) => typeof o === "string") : [];
363
+ return {
364
+ // A decision is the one step whose author already wrote it a human title.
365
+ title: str(cfg.title) ?? "Decision",
366
+ kind,
367
+ subtitle: options.length > 0 ? describeText(options.join(" / ")) : "Waits for a human",
368
+ description: describeText(cfg.prompt),
369
+ isRoot: false,
370
+ tone: "structural"
371
+ };
372
+ }
373
+ default:
374
+ return {
375
+ // An action kind this version doesn't model yet still gets a readable
376
+ // name ("agent.review" → "Agent review") rather than its raw identifier.
377
+ title: kind ? humanizeIdentifier(kind) : "Action",
378
+ kind: kind ?? "action",
379
+ isRoot: false,
380
+ tone: "action"
381
+ };
382
+ }
383
+ }
384
+ var MAX_CONFIG_DEPTH = 100;
385
+ function toJsonSafe(value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
386
+ if (value === null) return null;
387
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
388
+ if (typeof value === "string" || typeof value === "boolean") return value;
389
+ if (typeof value !== "object") return void 0;
390
+ if (value instanceof Date) return value.toISOString();
391
+ if (seen.has(value)) return "[Circular]";
392
+ if (depth >= MAX_CONFIG_DEPTH) return "[Max depth exceeded]";
393
+ seen.add(value);
394
+ let out;
395
+ if (Array.isArray(value)) {
396
+ out = value.map((v) => {
397
+ const s = toJsonSafe(v, seen, depth + 1);
398
+ return s === void 0 ? null : s;
399
+ });
400
+ } else {
401
+ const obj = {};
402
+ for (const [k, v] of Object.entries(value)) {
403
+ const s = toJsonSafe(v, seen, depth + 1);
404
+ if (s !== void 0) obj[k] = s;
405
+ }
406
+ out = obj;
407
+ }
408
+ seen.delete(value);
409
+ return out;
410
+ }
411
+ function withConfig(base, cfg) {
412
+ return Object.keys(cfg).length > 0 ? { ...base, config: toJsonSafe(cfg) } : base;
413
+ }
414
+ function describeAction(action) {
415
+ const rec = asRecord(action);
416
+ const kind = actionKind(rec);
417
+ const base = describeActionBase(action);
418
+ const config = toJsonSafe(asRecord(kind ? rec[kind] : {}));
419
+ for (const key of CONTROL_FLOW_KEYS) {
420
+ if (rec[key] !== void 0) config[key] = toJsonSafe(rec[key]);
421
+ }
422
+ return Object.keys(config).length > 0 ? { ...base, config } : base;
423
+ }
424
+ function describeTrigger(on) {
425
+ const rec = asRecord(on);
426
+ if (rec.provider_event) {
427
+ const ev = asRecord(rec.provider_event);
428
+ const connection = str(ev.connection);
429
+ const event = str(ev.event);
430
+ const actions = Array.isArray(ev.actions) ? ev.actions.filter((a) => typeof a === "string") : [];
431
+ const repo = str(ev.repo);
432
+ const description = describeText(
433
+ [actions.length > 0 ? actions.join(", ") : void 0, repo].filter(Boolean).join(" \xB7 ")
434
+ );
435
+ return withConfig(
436
+ {
437
+ title: connection ? providerLabel(connection) : "Trigger",
438
+ kind: "provider_event",
439
+ subtitle: event ? `On ${humanizeIdentifier(event, "lower")}` : "On an event",
440
+ description,
441
+ provider: connection,
442
+ isRoot: true,
443
+ tone: "trigger"
444
+ },
445
+ ev
446
+ );
447
+ }
448
+ if (rec.schedule) {
449
+ const sch = asRecord(rec.schedule);
450
+ const cron = str(sch.cron);
451
+ const tz = str(sch.timezone);
452
+ return withConfig(
453
+ {
454
+ title: "Schedule",
455
+ kind: "schedule",
456
+ // The cron in English; the expression itself stays in the description (and
457
+ // in full in the config) so the exact timetable is never hidden.
458
+ subtitle: cron ? describeCron(cron) : "On a timetable",
459
+ description: cron ? tz ? `${cron} \xB7 ${tz}` : cron : void 0,
460
+ isRoot: true,
461
+ tone: "trigger"
462
+ },
463
+ sch
464
+ );
465
+ }
466
+ const [kind] = Object.keys(rec);
467
+ return withConfig(
468
+ {
469
+ title: "Trigger",
470
+ kind: "trigger",
471
+ subtitle: kind ? humanizeIdentifier(kind) : void 0,
472
+ isRoot: true,
473
+ tone: "trigger"
474
+ },
475
+ asRecord(kind ? rec[kind] : void 0)
476
+ );
477
+ }
478
+ function layoutLayers(nodes, direction, rankSep, crossSep) {
479
+ const isLR = direction === "LR";
480
+ const mainSize = (n) => isLR ? n.width : n.height;
481
+ const crossSize = (n) => isLR ? n.height : n.width;
482
+ const byRank = /* @__PURE__ */ new Map();
483
+ for (const n of nodes) {
484
+ const arr = byRank.get(n.rank);
485
+ if (arr) arr.push(n);
486
+ else byRank.set(n.rank, [n]);
487
+ }
488
+ const ranks = [...byRank.keys()].sort((a, b) => a - b);
489
+ const mainStart = /* @__PURE__ */ new Map();
490
+ let cursor = 0;
491
+ for (const r of ranks) {
492
+ mainStart.set(r, cursor);
493
+ cursor += Math.max(...byRank.get(r).map(mainSize)) + rankSep;
494
+ }
495
+ const out = [];
496
+ for (const r of ranks) {
497
+ const layer = byRank.get(r);
498
+ const extent = Math.max(...layer.map(mainSize));
499
+ const span = layer.reduce((s, n) => s + crossSize(n), 0) + crossSep * (layer.length - 1);
500
+ let cross = -span / 2;
501
+ for (const n of layer) {
502
+ const main = mainStart.get(r) + (extent - mainSize(n)) / 2;
503
+ out.push({
504
+ id: n.id,
505
+ position: isLR ? { x: main, y: cross } : { x: cross, y: main },
506
+ width: n.width,
507
+ height: n.height,
508
+ data: n.data
509
+ });
510
+ cross += crossSize(n) + crossSep;
511
+ }
512
+ }
513
+ const minX = Math.min(...out.map((n) => n.position.x));
514
+ const minY = Math.min(...out.map((n) => n.position.y));
515
+ for (const n of out) {
516
+ n.position = { x: n.position.x - minX, y: n.position.y - minY };
517
+ }
518
+ return out;
519
+ }
520
+ function buildWorkflowGraph(yaml, options) {
521
+ const reserveRunState = options?.reserveRunState ?? false;
522
+ const direction = options?.direction ?? "LR";
523
+ const compact = options?.compact ?? false;
524
+ if (!yaml || yaml.trim() === "") {
525
+ return { nodes: [], edges: [], error: "No definition" };
526
+ }
527
+ let parsed;
528
+ try {
529
+ parsed = parseYaml(yaml);
530
+ } catch {
531
+ return { nodes: [], edges: [], error: "Invalid YAML" };
532
+ }
533
+ const def = asRecord(parsed);
534
+ const actions = Array.isArray(def.do) ? def.do : [];
535
+ if (!def.on && actions.length === 0) {
536
+ return { nodes: [], edges: [], error: "Empty workflow" };
537
+ }
538
+ const logical = [];
539
+ const edges = [];
540
+ const geo = geometry(compact, reserveRunState, direction);
541
+ const addNode = (id, data, rank2, withRunState) => {
542
+ const dims = geo.size ?? {
543
+ width: NODE_W,
544
+ height: nodeHeight(data, withRunState)
545
+ };
546
+ logical.push({ id, data, width: dims.width, height: dims.height, rank: rank2 });
547
+ };
548
+ const addEdge = (source, target, kind) => {
549
+ edges.push({ id: `${source}->${target}`, source, target, kind });
550
+ };
551
+ let rank = 0;
552
+ let prevExits = [];
553
+ const branchLeafIds = /* @__PURE__ */ new Set();
554
+ if (def.on) {
555
+ addNode("trigger", describeTrigger(def.on), rank, false);
556
+ prevExits = ["trigger"];
557
+ rank += 1;
558
+ }
559
+ actions.forEach((action, i) => {
560
+ const id = `a${i}`;
561
+ const data = describeAction(action);
562
+ if (i === 0 && !def.on) data.isRoot = true;
563
+ addNode(id, data, rank, reserveRunState);
564
+ for (const from of prevExits) {
565
+ addEdge(from, id, branchLeafIds.has(from) ? "join" : "spine");
566
+ }
567
+ const rec = asRecord(action);
568
+ const children = "parallel" in rec ? Array.isArray(asRecord(rec.parallel).branches) ? asRecord(rec.parallel).branches : [] : "foreach" in rec ? (
569
+ // A foreach with no `do` template has no child to render.
570
+ [asRecord(rec.foreach).do].filter(Boolean)
571
+ ) : [];
572
+ if (children.length > 0) {
573
+ const branchRank = rank + 1;
574
+ const branchIds = [];
575
+ children.forEach((child, j) => {
576
+ const cid = `${id}-b${j}`;
577
+ addNode(cid, describeAction(child), branchRank, reserveRunState);
578
+ addEdge(id, cid, "fork");
579
+ branchIds.push(cid);
580
+ branchLeafIds.add(cid);
581
+ });
582
+ prevExits = branchIds;
583
+ rank = branchRank + 1;
584
+ } else {
585
+ prevExits = [id];
586
+ rank += 1;
587
+ }
588
+ });
589
+ return {
590
+ nodes: layoutLayers(logical, direction, geo.rankSep, geo.crossSep),
591
+ edges,
592
+ error: null
593
+ };
594
+ }
595
+
596
+ export {
597
+ fmtDuration,
598
+ fmtCost,
599
+ clampPreview,
600
+ fmtTokens,
601
+ shortModel,
602
+ providerLabel,
603
+ COMPACT_TILE,
604
+ COMPACT_NODE_SIZE,
605
+ buildWorkflowGraph
606
+ };