@tangle-network/sandbox-ui 0.122.3 → 0.123.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,3 +1,7 @@
1
+ import { r as modelBrandFor, t as ModelBrandStack } from "./model-brand-hMyDTUqB.js";
2
+ import { t as ProviderIcon } from "./provider-logo-BiuiO_c2.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { AlertTriangle, Bell, Box, Cable, Camera, Circle, Clock, Code, GitBranch, Repeat, ScanSearch, Sparkles, UserCheck, Webhook, Zap } from "lucide-react";
1
5
  import { parse } from "yaml";
2
6
  //#region src/workflows/cron.ts
3
7
  /**
@@ -537,7 +541,10 @@ function urlHost(url) {
537
541
  const CONTROL_FLOW_KEYS = [
538
542
  "if",
539
543
  "retry",
540
- "onError"
544
+ "onError",
545
+ "id",
546
+ "needs",
547
+ "join"
541
548
  ];
542
549
  const CONTROL_FLOW = new Set(CONTROL_FLOW_KEYS);
543
550
  /**
@@ -572,7 +579,8 @@ const MINTED_PROFILE_ID = /^ap_[A-Za-z0-9_-]{16}$/;
572
579
  * a minted catalog id, has no readable name here — the node is the generic agent
573
580
  * rather than a mangled identifier. */
574
581
  function agentTitle(profile) {
575
- const named = str(profile);
582
+ const inline = asRecord(profile).name;
583
+ const named = str(profile) ?? str(inline);
576
584
  if (!named || MINTED_PROFILE_ID.test(named)) return "AI Agent";
577
585
  return humanizeIdentifier(named);
578
586
  }
@@ -1247,4 +1255,521 @@ function buildWorkflowGraph(yaml, options) {
1247
1255
  };
1248
1256
  }
1249
1257
  //#endregion
1250
- export { buildWorkflowGraph as a, triggerNodeIndex as c, providerLabel as d, shortModel as f, fmtTokens as g, fmtDuration as h, branchNodeId as i, wfEdgeId as l, fmtCost as m, TRIGGER_NODE_ID as n, isStructuralKind as o, clampPreview as p, actionNodeId as r, triggerNodeId as s, COMPACT_NODE_SIZE as t, worstSeverity as u };
1258
+ //#region src/workflows/node-ui.tsx
1259
+ /**
1260
+ * Shared presentational atoms for a workflow graph node — the type glyph, the
1261
+ * status color/label/badge maps, and the edge coloring. One source of truth so
1262
+ * the node card and the edges that point at it read as one system.
1263
+ *
1264
+ * Colors resolve to the SEMANTIC surface tokens (`--surface-success-text`,
1265
+ * `--surface-danger-text`, …), which carry a light AND a dark value tuned for
1266
+ * contrast against the card. A literal palette hex (`#22c55e`) or a stock
1267
+ * Tailwind shade (`text-red-400`) carries ONE value, so it can only ever hold its
1268
+ * contrast in one of the two themes — never use one for a status color.
1269
+ *
1270
+ * Inline `style` colors here use the RAW brand tokens (`hsl(var(--primary))`,
1271
+ * `var(--surface-…)`) — NOT the `--color-*` @theme aliases, which are undefined
1272
+ * in a consumer that only imports `tokens.css` (e.g. platform-web), where a
1273
+ * `var(--color-*)` would silently resolve to nothing. See the edge-color test.
1274
+ */
1275
+ /** Track color behind a progress bar — a faint wash of the muted token. */
1276
+ const MUTED_TRACK = "color-mix(in srgb, hsl(var(--muted-foreground)) 22%, transparent)";
1277
+ const TONE_ACCENT = {
1278
+ trigger: "hsl(var(--primary))",
1279
+ structural: "var(--surface-warning-text)",
1280
+ action: "hsl(var(--muted-foreground))"
1281
+ };
1282
+ const STATUS_QUEUED = "hsl(var(--muted-foreground))";
1283
+ const STATUS_COLOR = {
1284
+ queued: STATUS_QUEUED,
1285
+ running: "hsl(var(--primary))",
1286
+ waiting: "var(--surface-warning-text)",
1287
+ succeeded: "var(--surface-success-text)",
1288
+ failed: "var(--surface-danger-text)"
1289
+ };
1290
+ /** An edge is colored by the status of the node it points AT, so the run's
1291
+ * "front" lights up. Neutral for a not-yet-reached (queued) target or the
1292
+ * static definition view (`undefined`). */
1293
+ function edgeColor(status) {
1294
+ return status ? STATUS_COLOR[status] : STATUS_QUEUED;
1295
+ }
1296
+ const STATUS_LABEL = {
1297
+ queued: "Queued",
1298
+ running: "Running",
1299
+ waiting: "Waiting on you",
1300
+ succeeded: "Done",
1301
+ failed: "Failed"
1302
+ };
1303
+ /** Status pill styling — a tinted well in the status color. Built from the
1304
+ * semantic surface trio (bg/border/text) so the pill keeps its contrast in both
1305
+ * themes rather than washing out in one of them. */
1306
+ const STATUS_PILL = {
1307
+ queued: {
1308
+ background: "color-mix(in srgb, hsl(var(--muted-foreground)) 12%, transparent)",
1309
+ color: "hsl(var(--muted-foreground))",
1310
+ borderColor: "color-mix(in srgb, hsl(var(--muted-foreground)) 28%, transparent)"
1311
+ },
1312
+ running: {
1313
+ background: "color-mix(in srgb, hsl(var(--primary)) 14%, transparent)",
1314
+ color: "hsl(var(--primary))",
1315
+ borderColor: "color-mix(in srgb, hsl(var(--primary)) 40%, transparent)"
1316
+ },
1317
+ waiting: {
1318
+ background: "var(--surface-warning-bg)",
1319
+ color: "var(--surface-warning-text)",
1320
+ borderColor: "var(--surface-warning-border)"
1321
+ },
1322
+ succeeded: {
1323
+ background: "var(--surface-success-bg)",
1324
+ color: "var(--surface-success-text)",
1325
+ borderColor: "var(--surface-success-border)"
1326
+ },
1327
+ failed: {
1328
+ background: "var(--surface-danger-bg)",
1329
+ color: "var(--surface-danger-text)",
1330
+ borderColor: "var(--surface-danger-border)"
1331
+ }
1332
+ };
1333
+ /** The card's border + ring once a node has live run state, so the running node
1334
+ * and the terminal ones read at a glance. Returned as inline style (not classes)
1335
+ * because the status colors are semantic tokens, not palette shades. */
1336
+ function statusBorder(status) {
1337
+ const color = STATUS_COLOR[status];
1338
+ switch (status) {
1339
+ case "running":
1340
+ case "waiting": return {
1341
+ borderColor: color,
1342
+ boxShadow: `0 0 0 1px color-mix(in srgb, ${color} 45%, transparent), 0 0 24px -6px ${color}`
1343
+ };
1344
+ case "queued": return { borderColor: "hsl(var(--border))" };
1345
+ default: return {
1346
+ borderColor: color,
1347
+ boxShadow: `0 0 0 1px color-mix(in srgb, ${color} 35%, transparent)`
1348
+ };
1349
+ }
1350
+ }
1351
+ /** Determinate progress fraction for a node's status: queued reads near-empty,
1352
+ * running and waiting a partial (the run reached this node but has not finished
1353
+ * it), terminal full. Shared so every progress treatment (bar, footer, fill) maps
1354
+ * status → fill identically. A `waiting` bar is deliberately NOT animated by its
1355
+ * callers: the run is stopped there, and a moving bar would say otherwise. */
1356
+ function progressFill(status) {
1357
+ return status === "succeeded" || status === "failed" ? "100%" : status === "running" || status === "waiting" ? "58%" : "6%";
1358
+ }
1359
+ const KIND_ICON = {
1360
+ schedule: Clock,
1361
+ provider_event: Webhook,
1362
+ webhook: Webhook,
1363
+ trigger: Zap,
1364
+ "agent.run": Sparkles,
1365
+ "integration.invoke": Cable,
1366
+ notify: Bell,
1367
+ parallel: GitBranch,
1368
+ foreach: Repeat,
1369
+ decision: UserCheck,
1370
+ "sandbox.spawn": Box,
1371
+ "sandbox.snapshot": Camera,
1372
+ "script.run": Code,
1373
+ "trace.analyze": ScanSearch
1374
+ };
1375
+ /** An authoring problem's colours — the same semantic surface trio the run
1376
+ * statuses use, so a broken step and a failed one are recognisably the same
1377
+ * family of "something is wrong here" without being the same signal. */
1378
+ const PROBLEM_SURFACE_BY_SEVERITY = {
1379
+ error: {
1380
+ background: "var(--surface-danger-bg)",
1381
+ color: "var(--surface-danger-text)",
1382
+ borderColor: "var(--surface-danger-border)"
1383
+ },
1384
+ warning: {
1385
+ background: "var(--surface-warning-bg)",
1386
+ color: "var(--surface-warning-text)",
1387
+ borderColor: "var(--surface-warning-border)"
1388
+ }
1389
+ };
1390
+ /**
1391
+ * The colours a severity is drawn in. A lookup rather than the table itself, so
1392
+ * a severity this library has no entry for cannot throw its way out of a render
1393
+ * — the same resilience `problemSentence` and `worstSeverity` already give it,
1394
+ * and for the same reason: the package is consumed from JavaScript, and one
1395
+ * malformed problem must not take the canvas down. An unrecognised severity is
1396
+ * drawn as an error, matching how it is named and ranked.
1397
+ */
1398
+ function problemSurface(severity) {
1399
+ return PROBLEM_SURFACE_BY_SEVERITY[severity] ?? PROBLEM_SURFACE_BY_SEVERITY.error;
1400
+ }
1401
+ /** A node's border once it carries an authoring problem. Applied only where the
1402
+ * node has no RUN status to state — a run's border is the more urgent reading
1403
+ * of the same edge of the same box, and a definition being edited has no run. */
1404
+ function problemBorder(severity) {
1405
+ const color = problemSurface(severity).color;
1406
+ return {
1407
+ borderColor: color,
1408
+ boxShadow: `0 0 0 1px color-mix(in srgb, ${color} 35%, transparent)`
1409
+ };
1410
+ }
1411
+ /** How each severity is named where a colour cannot be seen. */
1412
+ const SEVERITY_WORD = {
1413
+ error: "Error",
1414
+ warning: "Warning"
1415
+ };
1416
+ /**
1417
+ * One problem as a reader receives it: WHICH KIND it is, then what it says.
1418
+ *
1419
+ * The kind has to be in the words. A mark shows one aggregate severity — as a
1420
+ * colour and a glyph, neither of which a screen reader receives — so an anchor
1421
+ * carrying an error and a warning together would otherwise offer no way to tell
1422
+ * which of the two is the one blocking the compile.
1423
+ *
1424
+ * NEWLINES are the only thing rewritten: each line is trimmed and the lines are
1425
+ * run together, so a multi-line host message cannot break the one-problem-per-
1426
+ * line reading below. Everything else survives — including internal spacing and
1427
+ * the trailing punctuation a diagnostic may be ABOUT ("unexpected ;") — because
1428
+ * the canvas has to say what the host's own problem list says. Separators are
1429
+ * handled at the join instead ({@link joinSentences}).
1430
+ *
1431
+ * A problem with no message at all is named by its severity alone, rather than
1432
+ * by a colon with nothing after it: nothing in {@link WfProblem} requires the
1433
+ * text to be non-empty, and "this step has an error" is still worth saying.
1434
+ */
1435
+ function problemSentence(problem) {
1436
+ const message = (typeof problem.message === "string" ? problem.message : "").split("\n").map((line) => line.trim()).filter(Boolean).join(" ");
1437
+ const word = SEVERITY_WORD[problem.severity] ?? SEVERITY_WORD.error;
1438
+ return message ? `${word}: ${message}` : word;
1439
+ }
1440
+ /** Run sentences together for a listener, adding a full stop only where the text
1441
+ * does not already end in one — so "Why?" is followed by a space rather than
1442
+ * "Why?.", and a message ending in a semicolon keeps it. Empty in, empty out:
1443
+ * `reduce` with no seed throws on an empty list, and this is reachable from any
1444
+ * caller holding an empty problem set. */
1445
+ function joinSentences(parts) {
1446
+ return parts.reduce((joined, part) => joined ? `${joined}${/[.!?;,]$/.test(joined) ? "" : "."} ${part}` : part, "");
1447
+ }
1448
+ /** Every problem on one anchor, as the tooltip text a marker hangs on. One per
1449
+ * line, so a step with three faults lists three rather than concatenating them
1450
+ * into a sentence nobody can parse. */
1451
+ function problemTitle(problems) {
1452
+ return problems.map(problemSentence).join("\n");
1453
+ }
1454
+ /**
1455
+ * Every message on one anchor, as the text a reader who cannot hover receives.
1456
+ * The visible mark has room for a glyph and a count, so the messages themselves
1457
+ * would otherwise live only in a `title` — which a pointer opens and a keyboard,
1458
+ * a screen reader and a touch device do not. Rendering them visually hidden puts
1459
+ * the same words in the accessibility tree, where they become the mark's own
1460
+ * accessible name.
1461
+ */
1462
+ function ProblemMessages({ problems }) {
1463
+ if (problems.length === 0) return null;
1464
+ return /* @__PURE__ */ jsx("span", {
1465
+ className: "sr-only",
1466
+ children: problems.length === 1 ? problemSentence(problems[0]) : `${problems.length} problems. ${joinSentences(problems.map(problemSentence))}`
1467
+ });
1468
+ }
1469
+ /**
1470
+ * The mark a node wears when the draft it is drawn from has a problem on it: a
1471
+ * warning glyph, with a count once there is more than one. It is a READOUT, not
1472
+ * a control — the node's own click already opens whatever the host shows for it,
1473
+ * and a second target inside the card would compete with that.
1474
+ *
1475
+ * It carries every message twice over: on `title`, where a pointer finds it, and
1476
+ * as visually hidden text, which is what the accessibility tree reads. That is
1477
+ * what makes the mark worth more than the border tint — the canvas says WHICH
1478
+ * step is broken at a glance and WHY without a trip to the problem list, however
1479
+ * the reader is driving it.
1480
+ */
1481
+ function ProblemMarker({ problems, className = "" }) {
1482
+ const severity = worstSeverity(problems) ?? "error";
1483
+ return /* @__PURE__ */ jsxs("span", {
1484
+ "data-testid": "wf-node-problem",
1485
+ "data-severity": severity,
1486
+ className: `flex shrink-0 items-center gap-0.5 rounded-full border px-1 py-[1px] font-medium text-[10px] leading-none ${className}`,
1487
+ style: problemSurface(severity),
1488
+ children: [/* @__PURE__ */ jsxs("span", {
1489
+ "aria-hidden": true,
1490
+ className: "flex items-center gap-0.5",
1491
+ title: problemTitle(problems),
1492
+ children: [/* @__PURE__ */ jsx(AlertTriangle, { size: 9 }), problems.length > 1 && /* @__PURE__ */ jsx("span", { children: problems.length })]
1493
+ }), /* @__PURE__ */ jsx(ProblemMessages, { problems })]
1494
+ });
1495
+ }
1496
+ /** The status pill in a card's header — the one place the run state is spelled
1497
+ * out in words. */
1498
+ function StatusPill({ status }) {
1499
+ return /* @__PURE__ */ jsx("span", {
1500
+ className: "shrink-0 whitespace-nowrap rounded-full border px-2 py-[1px] font-medium text-[10px]",
1501
+ style: STATUS_PILL[status],
1502
+ children: STATUS_LABEL[status]
1503
+ });
1504
+ }
1505
+ /**
1506
+ * The run status FOOTER: a progress bar (queued = near-empty, running = pulsing
1507
+ * partial, terminal = full) over a caption row. Pinned to the card's BOTTOM via
1508
+ * `mt-auto` so it lands at the same place on every node regardless of how much
1509
+ * content sits above it — unlike an inline strip, whose height drifts. The
1510
+ * caption reads agent rounds on the left and elapsed on the right; status itself
1511
+ * is carried by the bar color + the header pill, so it is NOT restated here. The
1512
+ * caption row always renders (even when empty) so the footer band keeps a
1513
+ * constant height across nodes.
1514
+ */
1515
+ function StatusFooter({ status, rounds, cost, tokens, elapsed }) {
1516
+ const caption = [
1517
+ rounds !== void 0 ? `${rounds} round${rounds === 1 ? "" : "s"}` : void 0,
1518
+ cost,
1519
+ tokens
1520
+ ].filter(Boolean).join(" · ");
1521
+ return /* @__PURE__ */ jsxs("div", {
1522
+ className: "wf-node-body-in mt-auto border-border border-t",
1523
+ children: [/* @__PURE__ */ jsx("div", {
1524
+ className: "h-1 w-full overflow-hidden",
1525
+ style: { background: MUTED_TRACK },
1526
+ children: /* @__PURE__ */ jsx("div", {
1527
+ "data-testid": "wf-node-progress",
1528
+ className: `h-full ${status === "running" ? "animate-pulse" : ""}`,
1529
+ ...status === "running" ? { "data-motion": "essential" } : {},
1530
+ style: {
1531
+ width: progressFill(status),
1532
+ background: STATUS_COLOR[status]
1533
+ }
1534
+ })
1535
+ }), /* @__PURE__ */ jsxs("div", {
1536
+ className: "flex items-center justify-between gap-2 px-3.5 py-1 text-[10px] text-muted-foreground leading-[15px]",
1537
+ children: [/* @__PURE__ */ jsx("span", {
1538
+ className: "min-h-[15px] truncate tabular-nums",
1539
+ children: caption
1540
+ }), /* @__PURE__ */ jsx("span", {
1541
+ className: "min-h-[15px] shrink-0 tabular-nums",
1542
+ children: elapsed ?? ""
1543
+ })]
1544
+ })]
1545
+ });
1546
+ }
1547
+ /**
1548
+ * What an expanded card says where its output WOULD be, when it has none. The
1549
+ * card is sized for the output it may yet have (the layout is computed once,
1550
+ * before any run state, so it can never reflow mid-run) — leaving that space
1551
+ * blank reads as a broken card rather than as a step with nothing to report. A
1552
+ * running node is the exception: its output may still be on its way, so it waits
1553
+ * quietly rather than claiming there is none.
1554
+ */
1555
+ function emptySlotLabel(status) {
1556
+ switch (status) {
1557
+ case "queued": return "Not run yet";
1558
+ case "succeeded": return "No output";
1559
+ case "failed": return "No error reported";
1560
+ default: return;
1561
+ }
1562
+ }
1563
+ /**
1564
+ * A node's MARK, centered in a tile the caller sizes: the provider's brand logo
1565
+ * when the step runs against a connector — the way n8n makes a node recognizable
1566
+ * before a word of it is read — else the kind's glyph, tinted by tone. Both
1567
+ * densities render the same mark at different sizes, so a node looks like itself
1568
+ * whether it's an icon tile or a full card.
1569
+ */
1570
+ function NodeMark({ kind, provider, model, accent, tile }) {
1571
+ const Icon = kind && KIND_ICON[kind] || Circle;
1572
+ const brand = !provider && model ? modelBrandFor(model) : null;
1573
+ const mark = Math.round(tile * (provider ? .58 : .5));
1574
+ return /* @__PURE__ */ jsx("span", {
1575
+ "aria-hidden": "true",
1576
+ className: "flex shrink-0 items-center justify-center overflow-hidden rounded-lg border border-border",
1577
+ style: {
1578
+ width: tile,
1579
+ height: tile,
1580
+ background: `color-mix(in srgb, ${accent} 10%, hsl(var(--card)))`
1581
+ },
1582
+ children: provider ? /* @__PURE__ */ jsx(ProviderIcon, {
1583
+ id: provider,
1584
+ displayName: providerLabel(provider),
1585
+ size: mark,
1586
+ className: "rounded-[3px]"
1587
+ }) : brand ? /* @__PURE__ */ jsx(ModelBrandStack, {
1588
+ identity: brand,
1589
+ size: brand.combined || tile >= 38 ? "md" : "sm"
1590
+ }) : /* @__PURE__ */ jsx(Icon, {
1591
+ size: mark,
1592
+ strokeWidth: 1.75,
1593
+ style: { color: accent }
1594
+ })
1595
+ });
1596
+ }
1597
+ //#endregion
1598
+ //#region src/workflows/node-output.tsx
1599
+ /** Failure text, in the semantic danger token — which carries a light AND a dark
1600
+ * value. A palette shade (`text-red-400`) only ever reads well in one theme. */
1601
+ const ERROR_TEXT = { color: "var(--surface-danger-text)" };
1602
+ /** Max key/value pairs a shallow-object output renders before it reports
1603
+ * `truncated` — the card only has room for a couple rows anyway. */
1604
+ const MAX_JSON_ENTRIES = 6;
1605
+ /**
1606
+ * A line that is pure structure — a code fence (with or without a language tag), a
1607
+ * table's rule row, a thematic break — and so carries no words to keep.
1608
+ *
1609
+ * Every test is a SINGLE quantified character class: unambiguous, and linear in the
1610
+ * length of the line. Written as one pattern (`[\s|:-]+\|[\s|:-]*`) the two
1611
+ * quantifiers would compete for the same `|`, which is the shape that backtracks.
1612
+ * Recognize the charset in one pass instead, then ask the cheap questions — is
1613
+ * there a pipe? a dash? — with plain string checks.
1614
+ */
1615
+ function isStructureOnly(line) {
1616
+ if (/^(```|~~~)/.test(line)) return true;
1617
+ const isBreak = /^([-*_])\1{2,}$/.test(line);
1618
+ if (/^[\s|:-]+$/.test(line) && line.includes("|")) return line.includes("-");
1619
+ return isBreak;
1620
+ }
1621
+ /**
1622
+ * Flatten an agent's markdown answer into the sentence a person would read out
1623
+ * of it. On a two-line card preview the syntax IS the noise: a `## Heading` mid
1624
+ * paragraph, a `**bold**` run and a bullet's `-` all survive the clamp while the
1625
+ * words that carry the meaning get pushed past it. Stripping the markup (never
1626
+ * the words) is what turns the preview back into prose — the full text, markup
1627
+ * and all, is still rendered by the node's detail view.
1628
+ *
1629
+ * Structure-only lines (a fence, a table rule) are dropped; everything else keeps
1630
+ * its text and is joined into one flowing line.
1631
+ */
1632
+ function condenseText(text) {
1633
+ const isListItem = (line) => /^\s*([-*+]\s|\d+[.)]\s)/.test(line);
1634
+ return text.split("\n").map((line) => ({
1635
+ item: isListItem(line),
1636
+ text: line.replace(/^\s{0,3}#{1,6}\s+/, "").replace(/^\s{0,3}>\s?/, "").replace(/^\s*[-*+]\s+(\[[ xX]\]\s+)?/, "").replace(/^\s*\d+[.)]\s+/, "").trim()
1637
+ })).filter(({ text: line }) => line !== "" && !isStructureOnly(line)).map(({ item, text: line }, i) => i > 0 && item ? `· ${line}` : line).join(" ").replace(/(?<=^|\s)(\*\*|~~)(?!\s)(.+?)(?<!\s)\1(?=[\s.,:;!?)\]}"']|$)/g, "$2").replace(/(?<![\w*])\*(?!\s)([^*]+?)(?<!\s)\*(?![\w*])/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\s+/g, " ").trim();
1638
+ }
1639
+ /** A single JSON value as a compact one-line string for the key/value render.
1640
+ * Scalars render verbatim; a nested object/array collapses to a shape marker so
1641
+ * one row never balloons into a wall of text. */
1642
+ function scalar(value) {
1643
+ if (value === null) return "null";
1644
+ if (typeof value === "string") return value;
1645
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
1646
+ if (Array.isArray(value)) return `[${value.length} item${value.length === 1 ? "" : "s"}]`;
1647
+ if (typeof value === "object") return "{…}";
1648
+ return "";
1649
+ }
1650
+ /**
1651
+ * Recover the shape of an output preview string. Only text that *begins* like
1652
+ * JSON (`{`/`[`) is parse-attempted, so ordinary prose starting with a brace-free
1653
+ * sentence is never mis-parsed. A begins-like-JSON string that fails to parse
1654
+ * (the host clamped it mid-value) still renders as `code`, not prose, so it keeps
1655
+ * its monospace/data affordance.
1656
+ *
1657
+ * `condenseMarkdown` says the prose IS markdown and may be flattened for the
1658
+ * preview ({@link condenseText}). It is OFF by default, and the caller must only
1659
+ * turn it on for text an agent actually authored as markdown. Applied to anything
1660
+ * else it does not merely look wrong — it silently REWRITES the text: a stack
1661
+ * trace's `__init__.py` loses its underscores, and a shell glob `src/**` pairs
1662
+ * with a later `**` and both vanish. An error message is the one string a person
1663
+ * reads to find out what broke; it must reach them exactly as it was thrown.
1664
+ */
1665
+ function classifyOutput(preview, condenseMarkdown = false) {
1666
+ let trimmed = preview?.trim() ?? "";
1667
+ trimmed = trimmed.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/g, "").replace(/(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "").trimEnd();
1668
+ if (trimmed === "") return { kind: "empty" };
1669
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) try {
1670
+ const parsed = JSON.parse(trimmed);
1671
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1672
+ const all = Object.entries(parsed);
1673
+ const entries = all.slice(0, MAX_JSON_ENTRIES).map(([k, v]) => [k, scalar(v)]);
1674
+ if (entries.length > 0) return {
1675
+ kind: "json",
1676
+ entries,
1677
+ truncated: all.length > entries.length
1678
+ };
1679
+ }
1680
+ return {
1681
+ kind: "code",
1682
+ text: trimmed
1683
+ };
1684
+ } catch {
1685
+ return {
1686
+ kind: "code",
1687
+ text: trimmed
1688
+ };
1689
+ }
1690
+ if (!condenseMarkdown) return {
1691
+ kind: "text",
1692
+ text: trimmed
1693
+ };
1694
+ const condensed = condenseText(trimmed);
1695
+ return condensed === "" ? { kind: "empty" } : {
1696
+ kind: "text",
1697
+ text: condensed
1698
+ };
1699
+ }
1700
+ /** Line-clamp utility per row budget, so `code`/`text` shapes honor `rows` the
1701
+ * same way the JSON branch does (rather than always clamping to two). Covers the
1702
+ * full range a card can reserve; an unsupported count falls back to the two-line
1703
+ * clamp, which renders FEWER lines than the caller asked for — so a card whose
1704
+ * reservation outgrows this map shows a short body inside a tall well rather
1705
+ * than erroring. Extend the map when a design reserves more.
1706
+ * Class names are literal so the Tailwind scanner emits each utility. */
1707
+ const CLAMP_BY_ROWS = {
1708
+ 1: "truncate",
1709
+ 2: "line-clamp-2",
1710
+ 3: "line-clamp-3",
1711
+ 4: "line-clamp-4",
1712
+ 5: "line-clamp-5",
1713
+ 6: "line-clamp-6"
1714
+ };
1715
+ /**
1716
+ * Render a classified output preview's CONTENT (no surrounding frame — each node
1717
+ * design supplies its own container/label). `rows` caps the visible lines so the
1718
+ * body stays within a card's reserved output space; `tone="error"` colors it for
1719
+ * a failure message.
1720
+ *
1721
+ * `rows="none"` renders the body UNCLAMPED, for a container that scrolls or is
1722
+ * sized by its content — a drawer, a detail panel, a selection overlay. It is a
1723
+ * word rather than a large number because there is no number that means "do not
1724
+ * clamp": a count outside {@link CLAMP_BY_ROWS} falls back to two lines, so
1725
+ * `rows={99}` asks for everything and silently shows the least.
1726
+ */
1727
+ function NodeOutputBody({ shape, rows = 2, tone = "default" }) {
1728
+ const unclamped = rows === "none";
1729
+ const clampClass = unclamped ? "" : CLAMP_BY_ROWS[rows] ?? "line-clamp-2";
1730
+ const codeWrap = rows === 1 ? "" : "whitespace-pre-wrap break-all";
1731
+ if (shape.kind === "empty") return null;
1732
+ if (shape.kind === "json") {
1733
+ const overflow = unclamped ? shape.truncated : shape.truncated || shape.entries.length > rows;
1734
+ const visible = unclamped ? shape.entries : shape.entries.slice(0, overflow ? Math.max(0, rows - 1) : rows);
1735
+ const isError = tone === "error";
1736
+ const keyStyle = isError ? ERROR_TEXT : void 0;
1737
+ const keyClass = isError ? "" : "text-muted-foreground";
1738
+ const valueClass = isError ? "" : "text-foreground";
1739
+ return /* @__PURE__ */ jsxs("dl", {
1740
+ className: "space-y-0.5 font-mono text-[10.5px] leading-snug",
1741
+ children: [visible.map(([key, value]) => /* @__PURE__ */ jsxs("div", {
1742
+ className: "flex gap-1.5",
1743
+ children: [/* @__PURE__ */ jsx("dt", {
1744
+ className: `max-w-[45%] shrink-0 truncate ${keyClass}`,
1745
+ style: keyStyle,
1746
+ title: key,
1747
+ children: key
1748
+ }), /* @__PURE__ */ jsx("dd", {
1749
+ className: `min-w-0 flex-1 truncate ${valueClass}`,
1750
+ style: isError ? ERROR_TEXT : void 0,
1751
+ title: value,
1752
+ children: value
1753
+ })]
1754
+ }, key)), overflow && /* @__PURE__ */ jsx("div", {
1755
+ className: `text-[10px] ${keyClass}`,
1756
+ style: keyStyle,
1757
+ children: "…"
1758
+ })]
1759
+ });
1760
+ }
1761
+ if (shape.kind === "code") return /* @__PURE__ */ jsx("pre", {
1762
+ className: `${clampClass} ${codeWrap} font-mono text-[10.5px] leading-snug ${tone === "error" ? "" : "text-foreground"}`,
1763
+ style: tone === "error" ? ERROR_TEXT : void 0,
1764
+ title: shape.text,
1765
+ children: shape.text
1766
+ });
1767
+ return /* @__PURE__ */ jsx("p", {
1768
+ className: `${clampClass} text-[11px] leading-snug ${tone === "error" ? "" : "text-foreground"}`,
1769
+ style: tone === "error" ? ERROR_TEXT : void 0,
1770
+ title: shape.text,
1771
+ children: shape.text
1772
+ });
1773
+ }
1774
+ //#endregion
1775
+ export { actionPathLabel as A, branchNodeId as C, triggerNodeIndex as D, triggerNodeId as E, fmtCost as F, fmtDuration as I, fmtTokens as L, parseActionPath as M, shortModel as N, wfEdgeId as O, clampPreview as P, actionNodeId as S, isStructuralKind as T, problemSurface as _, NodeMark as a, COMPACT_NODE_SIZE as b, STATUS_COLOR as c, StatusFooter as d, StatusPill as f, problemBorder as g, emptySlotLabel as h, KIND_ICON as i, humanizeIdentifier as j, worstSeverity as k, STATUS_LABEL as l, edgeColor as m, classifyOutput as n, ProblemMarker as o, TONE_ACCENT as p, condenseText as r, ProblemMessages as s, NodeOutputBody as t, STATUS_PILL as u, problemTitle as v, buildWorkflowGraph as w, TRIGGER_NODE_ID as x, statusBorder as y };