@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.
- package/dist/{WorkflowGraph-E2Gqe8sl.js → WorkflowGraph-B4mkYGe3.js} +2 -521
- package/dist/globals.css +106 -26
- package/dist/{model-I-NIxeM-.js → node-output-D-fYilAu.js} +528 -3
- package/dist/styles.css +106 -26
- package/dist/tokens.css +8 -8
- package/dist/workflows.d.ts +166 -1
- package/dist/workflows.js +3 -3
- package/package.json +5 -5
|
@@ -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
|
|
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
|
-
|
|
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 };
|