@tangle-network/sandbox-ui 0.122.3 → 0.123.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.
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as buildWorkflowGraph, c as triggerNodeIndex, d as providerLabel, f as shortModel, g as fmtTokens, h as fmtDuration, l as wfEdgeId, m as fmtCost, o as isStructuralKind, p as clampPreview, t as COMPACT_NODE_SIZE, u as worstSeverity } from "./model-I-NIxeM-.js";
|
|
3
|
-
import { t as ProviderIcon } from "./provider-logo-BiuiO_c2.js";
|
|
1
|
+
import { D as triggerNodeIndex, F as fmtCost, I as fmtDuration, L as fmtTokens, N as shortModel, O as wfEdgeId, P as clampPreview, T as isStructuralKind, _ as problemSurface, a as NodeMark, b as COMPACT_NODE_SIZE, c as STATUS_COLOR, d as StatusFooter, f as StatusPill, g as problemBorder, h as emptySlotLabel, k as worstSeverity, l as STATUS_LABEL, m as edgeColor, n as classifyOutput, o as ProblemMarker, p as TONE_ACCENT, s as ProblemMessages, t as NodeOutputBody, v as problemTitle, w as buildWorkflowGraph, y as statusBorder } from "./node-output-D-fYilAu.js";
|
|
4
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
3
|
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
6
|
-
import {
|
|
4
|
+
import { Maximize2, Minimize2, Plus, X } from "lucide-react";
|
|
7
5
|
import { Background, BaseEdge, Controls, EdgeLabelRenderer, Handle, MarkerType, Panel, Position, ReactFlow, getSmoothStepPath, getViewportForBounds, useEdgesState, useNodesState } from "@xyflow/react";
|
|
8
6
|
import "@xyflow/react/dist/style.css";
|
|
9
7
|
//#region src/workflows/flow-graph.ts
|
|
@@ -289,523 +287,6 @@ function clearOfNodeBoxes(point, boxes, direction, half) {
|
|
|
289
287
|
return Math.abs(up) <= Math.abs(down) ? up : down;
|
|
290
288
|
}
|
|
291
289
|
//#endregion
|
|
292
|
-
//#region src/workflows/node-output.tsx
|
|
293
|
-
/** Failure text, in the semantic danger token — which carries a light AND a dark
|
|
294
|
-
* value. A palette shade (`text-red-400`) only ever reads well in one theme. */
|
|
295
|
-
const ERROR_TEXT = { color: "var(--surface-danger-text)" };
|
|
296
|
-
/** Max key/value pairs a shallow-object output renders before it reports
|
|
297
|
-
* `truncated` — the card only has room for a couple rows anyway. */
|
|
298
|
-
const MAX_JSON_ENTRIES = 6;
|
|
299
|
-
/**
|
|
300
|
-
* A line that is pure structure — a code fence (with or without a language tag), a
|
|
301
|
-
* table's rule row, a thematic break — and so carries no words to keep.
|
|
302
|
-
*
|
|
303
|
-
* Every test is a SINGLE quantified character class: unambiguous, and linear in the
|
|
304
|
-
* length of the line. Written as one pattern (`[\s|:-]+\|[\s|:-]*`) the two
|
|
305
|
-
* quantifiers would compete for the same `|`, which is the shape that backtracks.
|
|
306
|
-
* Recognize the charset in one pass instead, then ask the cheap questions — is
|
|
307
|
-
* there a pipe? a dash? — with plain string checks.
|
|
308
|
-
*/
|
|
309
|
-
function isStructureOnly(line) {
|
|
310
|
-
if (/^(```|~~~)/.test(line)) return true;
|
|
311
|
-
const isBreak = /^([-*_])\1{2,}$/.test(line);
|
|
312
|
-
if (/^[\s|:-]+$/.test(line) && line.includes("|")) return line.includes("-");
|
|
313
|
-
return isBreak;
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Flatten an agent's markdown answer into the sentence a person would read out
|
|
317
|
-
* of it. On a two-line card preview the syntax IS the noise: a `## Heading` mid
|
|
318
|
-
* paragraph, a `**bold**` run and a bullet's `-` all survive the clamp while the
|
|
319
|
-
* words that carry the meaning get pushed past it. Stripping the markup (never
|
|
320
|
-
* the words) is what turns the preview back into prose — the full text, markup
|
|
321
|
-
* and all, is still rendered by the node's detail view.
|
|
322
|
-
*
|
|
323
|
-
* Structure-only lines (a fence, a table rule) are dropped; everything else keeps
|
|
324
|
-
* its text and is joined into one flowing line.
|
|
325
|
-
*/
|
|
326
|
-
function condenseText(text) {
|
|
327
|
-
const isListItem = (line) => /^\s*([-*+]\s|\d+[.)]\s)/.test(line);
|
|
328
|
-
return text.split("\n").map((line) => ({
|
|
329
|
-
item: isListItem(line),
|
|
330
|
-
text: line.replace(/^\s{0,3}#{1,6}\s+/, "").replace(/^\s{0,3}>\s?/, "").replace(/^\s*[-*+]\s+(\[[ xX]\]\s+)?/, "").replace(/^\s*\d+[.)]\s+/, "").trim()
|
|
331
|
-
})).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();
|
|
332
|
-
}
|
|
333
|
-
/** A single JSON value as a compact one-line string for the key/value render.
|
|
334
|
-
* Scalars render verbatim; a nested object/array collapses to a shape marker so
|
|
335
|
-
* one row never balloons into a wall of text. */
|
|
336
|
-
function scalar(value) {
|
|
337
|
-
if (value === null) return "null";
|
|
338
|
-
if (typeof value === "string") return value;
|
|
339
|
-
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
340
|
-
if (Array.isArray(value)) return `[${value.length} item${value.length === 1 ? "" : "s"}]`;
|
|
341
|
-
if (typeof value === "object") return "{…}";
|
|
342
|
-
return "";
|
|
343
|
-
}
|
|
344
|
-
/**
|
|
345
|
-
* Recover the shape of an output preview string. Only text that *begins* like
|
|
346
|
-
* JSON (`{`/`[`) is parse-attempted, so ordinary prose starting with a brace-free
|
|
347
|
-
* sentence is never mis-parsed. A begins-like-JSON string that fails to parse
|
|
348
|
-
* (the host clamped it mid-value) still renders as `code`, not prose, so it keeps
|
|
349
|
-
* its monospace/data affordance.
|
|
350
|
-
*
|
|
351
|
-
* `condenseMarkdown` says the prose IS markdown and may be flattened for the
|
|
352
|
-
* preview ({@link condenseText}). It is OFF by default, and the caller must only
|
|
353
|
-
* turn it on for text an agent actually authored as markdown. Applied to anything
|
|
354
|
-
* else it does not merely look wrong — it silently REWRITES the text: a stack
|
|
355
|
-
* trace's `__init__.py` loses its underscores, and a shell glob `src/**` pairs
|
|
356
|
-
* with a later `**` and both vanish. An error message is the one string a person
|
|
357
|
-
* reads to find out what broke; it must reach them exactly as it was thrown.
|
|
358
|
-
*/
|
|
359
|
-
function classifyOutput(preview, condenseMarkdown = false) {
|
|
360
|
-
let trimmed = preview?.trim() ?? "";
|
|
361
|
-
trimmed = trimmed.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/g, "").replace(/(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "").trimEnd();
|
|
362
|
-
if (trimmed === "") return { kind: "empty" };
|
|
363
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[")) try {
|
|
364
|
-
const parsed = JSON.parse(trimmed);
|
|
365
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
366
|
-
const all = Object.entries(parsed);
|
|
367
|
-
const entries = all.slice(0, MAX_JSON_ENTRIES).map(([k, v]) => [k, scalar(v)]);
|
|
368
|
-
if (entries.length > 0) return {
|
|
369
|
-
kind: "json",
|
|
370
|
-
entries,
|
|
371
|
-
truncated: all.length > entries.length
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
return {
|
|
375
|
-
kind: "code",
|
|
376
|
-
text: trimmed
|
|
377
|
-
};
|
|
378
|
-
} catch {
|
|
379
|
-
return {
|
|
380
|
-
kind: "code",
|
|
381
|
-
text: trimmed
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
if (!condenseMarkdown) return {
|
|
385
|
-
kind: "text",
|
|
386
|
-
text: trimmed
|
|
387
|
-
};
|
|
388
|
-
const condensed = condenseText(trimmed);
|
|
389
|
-
return condensed === "" ? { kind: "empty" } : {
|
|
390
|
-
kind: "text",
|
|
391
|
-
text: condensed
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
/** Line-clamp utility per row budget, so `code`/`text` shapes honor `rows` the
|
|
395
|
-
* same way the JSON branch does (rather than always clamping to two). Covers the
|
|
396
|
-
* full range a card can reserve; an unsupported count falls back to the two-line
|
|
397
|
-
* clamp, which renders FEWER lines than the caller asked for — so a card whose
|
|
398
|
-
* reservation outgrows this map shows a short body inside a tall well rather
|
|
399
|
-
* than erroring. Extend the map when a design reserves more.
|
|
400
|
-
* Class names are literal so the Tailwind scanner emits each utility. */
|
|
401
|
-
const CLAMP_BY_ROWS = {
|
|
402
|
-
1: "truncate",
|
|
403
|
-
2: "line-clamp-2",
|
|
404
|
-
3: "line-clamp-3",
|
|
405
|
-
4: "line-clamp-4",
|
|
406
|
-
5: "line-clamp-5",
|
|
407
|
-
6: "line-clamp-6"
|
|
408
|
-
};
|
|
409
|
-
/**
|
|
410
|
-
* Render a classified output preview's CONTENT (no surrounding frame — each node
|
|
411
|
-
* design supplies its own container/label). `rows` caps the visible lines so the
|
|
412
|
-
* body stays within a card's reserved output space; `tone="error"` colors it for
|
|
413
|
-
* a failure message.
|
|
414
|
-
*
|
|
415
|
-
* `rows="none"` renders the body UNCLAMPED, for a container that scrolls or is
|
|
416
|
-
* sized by its content — a drawer, a detail panel, a selection overlay. It is a
|
|
417
|
-
* word rather than a large number because there is no number that means "do not
|
|
418
|
-
* clamp": a count outside {@link CLAMP_BY_ROWS} falls back to two lines, so
|
|
419
|
-
* `rows={99}` asks for everything and silently shows the least.
|
|
420
|
-
*/
|
|
421
|
-
function NodeOutputBody({ shape, rows = 2, tone = "default" }) {
|
|
422
|
-
const unclamped = rows === "none";
|
|
423
|
-
const clampClass = unclamped ? "" : CLAMP_BY_ROWS[rows] ?? "line-clamp-2";
|
|
424
|
-
const codeWrap = rows === 1 ? "" : "whitespace-pre-wrap break-all";
|
|
425
|
-
if (shape.kind === "empty") return null;
|
|
426
|
-
if (shape.kind === "json") {
|
|
427
|
-
const overflow = unclamped ? shape.truncated : shape.truncated || shape.entries.length > rows;
|
|
428
|
-
const visible = unclamped ? shape.entries : shape.entries.slice(0, overflow ? Math.max(0, rows - 1) : rows);
|
|
429
|
-
const isError = tone === "error";
|
|
430
|
-
const keyStyle = isError ? ERROR_TEXT : void 0;
|
|
431
|
-
const keyClass = isError ? "" : "text-muted-foreground";
|
|
432
|
-
const valueClass = isError ? "" : "text-foreground";
|
|
433
|
-
return /* @__PURE__ */ jsxs("dl", {
|
|
434
|
-
className: "space-y-0.5 font-mono text-[10.5px] leading-snug",
|
|
435
|
-
children: [visible.map(([key, value]) => /* @__PURE__ */ jsxs("div", {
|
|
436
|
-
className: "flex gap-1.5",
|
|
437
|
-
children: [/* @__PURE__ */ jsx("dt", {
|
|
438
|
-
className: `max-w-[45%] shrink-0 truncate ${keyClass}`,
|
|
439
|
-
style: keyStyle,
|
|
440
|
-
title: key,
|
|
441
|
-
children: key
|
|
442
|
-
}), /* @__PURE__ */ jsx("dd", {
|
|
443
|
-
className: `min-w-0 flex-1 truncate ${valueClass}`,
|
|
444
|
-
style: isError ? ERROR_TEXT : void 0,
|
|
445
|
-
title: value,
|
|
446
|
-
children: value
|
|
447
|
-
})]
|
|
448
|
-
}, key)), overflow && /* @__PURE__ */ jsx("div", {
|
|
449
|
-
className: `text-[10px] ${keyClass}`,
|
|
450
|
-
style: keyStyle,
|
|
451
|
-
children: "…"
|
|
452
|
-
})]
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
if (shape.kind === "code") return /* @__PURE__ */ jsx("pre", {
|
|
456
|
-
className: `${clampClass} ${codeWrap} font-mono text-[10.5px] leading-snug ${tone === "error" ? "" : "text-foreground"}`,
|
|
457
|
-
style: tone === "error" ? ERROR_TEXT : void 0,
|
|
458
|
-
title: shape.text,
|
|
459
|
-
children: shape.text
|
|
460
|
-
});
|
|
461
|
-
return /* @__PURE__ */ jsx("p", {
|
|
462
|
-
className: `${clampClass} text-[11px] leading-snug ${tone === "error" ? "" : "text-foreground"}`,
|
|
463
|
-
style: tone === "error" ? ERROR_TEXT : void 0,
|
|
464
|
-
title: shape.text,
|
|
465
|
-
children: shape.text
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
//#endregion
|
|
469
|
-
//#region src/workflows/node-ui.tsx
|
|
470
|
-
/**
|
|
471
|
-
* Shared presentational atoms for a workflow graph node — the type glyph, the
|
|
472
|
-
* status color/label/badge maps, and the edge coloring. One source of truth so
|
|
473
|
-
* the node card and the edges that point at it read as one system.
|
|
474
|
-
*
|
|
475
|
-
* Colors resolve to the SEMANTIC surface tokens (`--surface-success-text`,
|
|
476
|
-
* `--surface-danger-text`, …), which carry a light AND a dark value tuned for
|
|
477
|
-
* contrast against the card. A literal palette hex (`#22c55e`) or a stock
|
|
478
|
-
* Tailwind shade (`text-red-400`) carries ONE value, so it can only ever hold its
|
|
479
|
-
* contrast in one of the two themes — never use one for a status color.
|
|
480
|
-
*
|
|
481
|
-
* Inline `style` colors here use the RAW brand tokens (`hsl(var(--primary))`,
|
|
482
|
-
* `var(--surface-…)`) — NOT the `--color-*` @theme aliases, which are undefined
|
|
483
|
-
* in a consumer that only imports `tokens.css` (e.g. platform-web), where a
|
|
484
|
-
* `var(--color-*)` would silently resolve to nothing. See the edge-color test.
|
|
485
|
-
*/
|
|
486
|
-
/** Track color behind a progress bar — a faint wash of the muted token. */
|
|
487
|
-
const MUTED_TRACK = "color-mix(in srgb, hsl(var(--muted-foreground)) 22%, transparent)";
|
|
488
|
-
const TONE_ACCENT = {
|
|
489
|
-
trigger: "hsl(var(--primary))",
|
|
490
|
-
structural: "var(--surface-warning-text)",
|
|
491
|
-
action: "hsl(var(--muted-foreground))"
|
|
492
|
-
};
|
|
493
|
-
const STATUS_QUEUED = "hsl(var(--muted-foreground))";
|
|
494
|
-
const STATUS_COLOR = {
|
|
495
|
-
queued: STATUS_QUEUED,
|
|
496
|
-
running: "hsl(var(--primary))",
|
|
497
|
-
waiting: "var(--surface-warning-text)",
|
|
498
|
-
succeeded: "var(--surface-success-text)",
|
|
499
|
-
failed: "var(--surface-danger-text)"
|
|
500
|
-
};
|
|
501
|
-
/** An edge is colored by the status of the node it points AT, so the run's
|
|
502
|
-
* "front" lights up. Neutral for a not-yet-reached (queued) target or the
|
|
503
|
-
* static definition view (`undefined`). */
|
|
504
|
-
function edgeColor(status) {
|
|
505
|
-
return status ? STATUS_COLOR[status] : STATUS_QUEUED;
|
|
506
|
-
}
|
|
507
|
-
const STATUS_LABEL = {
|
|
508
|
-
queued: "Queued",
|
|
509
|
-
running: "Running",
|
|
510
|
-
waiting: "Waiting on you",
|
|
511
|
-
succeeded: "Done",
|
|
512
|
-
failed: "Failed"
|
|
513
|
-
};
|
|
514
|
-
/** Status pill styling — a tinted well in the status color. Built from the
|
|
515
|
-
* semantic surface trio (bg/border/text) so the pill keeps its contrast in both
|
|
516
|
-
* themes rather than washing out in one of them. */
|
|
517
|
-
const STATUS_PILL = {
|
|
518
|
-
queued: {
|
|
519
|
-
background: "color-mix(in srgb, hsl(var(--muted-foreground)) 12%, transparent)",
|
|
520
|
-
color: "hsl(var(--muted-foreground))",
|
|
521
|
-
borderColor: "color-mix(in srgb, hsl(var(--muted-foreground)) 28%, transparent)"
|
|
522
|
-
},
|
|
523
|
-
running: {
|
|
524
|
-
background: "color-mix(in srgb, hsl(var(--primary)) 14%, transparent)",
|
|
525
|
-
color: "hsl(var(--primary))",
|
|
526
|
-
borderColor: "color-mix(in srgb, hsl(var(--primary)) 40%, transparent)"
|
|
527
|
-
},
|
|
528
|
-
waiting: {
|
|
529
|
-
background: "var(--surface-warning-bg)",
|
|
530
|
-
color: "var(--surface-warning-text)",
|
|
531
|
-
borderColor: "var(--surface-warning-border)"
|
|
532
|
-
},
|
|
533
|
-
succeeded: {
|
|
534
|
-
background: "var(--surface-success-bg)",
|
|
535
|
-
color: "var(--surface-success-text)",
|
|
536
|
-
borderColor: "var(--surface-success-border)"
|
|
537
|
-
},
|
|
538
|
-
failed: {
|
|
539
|
-
background: "var(--surface-danger-bg)",
|
|
540
|
-
color: "var(--surface-danger-text)",
|
|
541
|
-
borderColor: "var(--surface-danger-border)"
|
|
542
|
-
}
|
|
543
|
-
};
|
|
544
|
-
/** The card's border + ring once a node has live run state, so the running node
|
|
545
|
-
* and the terminal ones read at a glance. Returned as inline style (not classes)
|
|
546
|
-
* because the status colors are semantic tokens, not palette shades. */
|
|
547
|
-
function statusBorder(status) {
|
|
548
|
-
const color = STATUS_COLOR[status];
|
|
549
|
-
switch (status) {
|
|
550
|
-
case "running":
|
|
551
|
-
case "waiting": return {
|
|
552
|
-
borderColor: color,
|
|
553
|
-
boxShadow: `0 0 0 1px color-mix(in srgb, ${color} 45%, transparent), 0 0 24px -6px ${color}`
|
|
554
|
-
};
|
|
555
|
-
case "queued": return { borderColor: "hsl(var(--border))" };
|
|
556
|
-
default: return {
|
|
557
|
-
borderColor: color,
|
|
558
|
-
boxShadow: `0 0 0 1px color-mix(in srgb, ${color} 35%, transparent)`
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
/** Determinate progress fraction for a node's status: queued reads near-empty,
|
|
563
|
-
* running and waiting a partial (the run reached this node but has not finished
|
|
564
|
-
* it), terminal full. Shared so every progress treatment (bar, footer, fill) maps
|
|
565
|
-
* status → fill identically. A `waiting` bar is deliberately NOT animated by its
|
|
566
|
-
* callers: the run is stopped there, and a moving bar would say otherwise. */
|
|
567
|
-
function progressFill(status) {
|
|
568
|
-
return status === "succeeded" || status === "failed" ? "100%" : status === "running" || status === "waiting" ? "58%" : "6%";
|
|
569
|
-
}
|
|
570
|
-
const KIND_ICON = {
|
|
571
|
-
schedule: Clock,
|
|
572
|
-
provider_event: Webhook,
|
|
573
|
-
webhook: Webhook,
|
|
574
|
-
trigger: Zap,
|
|
575
|
-
"agent.run": Sparkles,
|
|
576
|
-
"integration.invoke": Cable,
|
|
577
|
-
notify: Bell,
|
|
578
|
-
parallel: GitBranch,
|
|
579
|
-
foreach: Repeat,
|
|
580
|
-
decision: UserCheck,
|
|
581
|
-
"sandbox.spawn": Box,
|
|
582
|
-
"sandbox.snapshot": Camera,
|
|
583
|
-
"script.run": Code,
|
|
584
|
-
"trace.analyze": ScanSearch
|
|
585
|
-
};
|
|
586
|
-
/** An authoring problem's colours — the same semantic surface trio the run
|
|
587
|
-
* statuses use, so a broken step and a failed one are recognisably the same
|
|
588
|
-
* family of "something is wrong here" without being the same signal. */
|
|
589
|
-
const PROBLEM_SURFACE_BY_SEVERITY = {
|
|
590
|
-
error: {
|
|
591
|
-
background: "var(--surface-danger-bg)",
|
|
592
|
-
color: "var(--surface-danger-text)",
|
|
593
|
-
borderColor: "var(--surface-danger-border)"
|
|
594
|
-
},
|
|
595
|
-
warning: {
|
|
596
|
-
background: "var(--surface-warning-bg)",
|
|
597
|
-
color: "var(--surface-warning-text)",
|
|
598
|
-
borderColor: "var(--surface-warning-border)"
|
|
599
|
-
}
|
|
600
|
-
};
|
|
601
|
-
/**
|
|
602
|
-
* The colours a severity is drawn in. A lookup rather than the table itself, so
|
|
603
|
-
* a severity this library has no entry for cannot throw its way out of a render
|
|
604
|
-
* — the same resilience `problemSentence` and `worstSeverity` already give it,
|
|
605
|
-
* and for the same reason: the package is consumed from JavaScript, and one
|
|
606
|
-
* malformed problem must not take the canvas down. An unrecognised severity is
|
|
607
|
-
* drawn as an error, matching how it is named and ranked.
|
|
608
|
-
*/
|
|
609
|
-
function problemSurface(severity) {
|
|
610
|
-
return PROBLEM_SURFACE_BY_SEVERITY[severity] ?? PROBLEM_SURFACE_BY_SEVERITY.error;
|
|
611
|
-
}
|
|
612
|
-
/** A node's border once it carries an authoring problem. Applied only where the
|
|
613
|
-
* node has no RUN status to state — a run's border is the more urgent reading
|
|
614
|
-
* of the same edge of the same box, and a definition being edited has no run. */
|
|
615
|
-
function problemBorder(severity) {
|
|
616
|
-
const color = problemSurface(severity).color;
|
|
617
|
-
return {
|
|
618
|
-
borderColor: color,
|
|
619
|
-
boxShadow: `0 0 0 1px color-mix(in srgb, ${color} 35%, transparent)`
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
/** How each severity is named where a colour cannot be seen. */
|
|
623
|
-
const SEVERITY_WORD = {
|
|
624
|
-
error: "Error",
|
|
625
|
-
warning: "Warning"
|
|
626
|
-
};
|
|
627
|
-
/**
|
|
628
|
-
* One problem as a reader receives it: WHICH KIND it is, then what it says.
|
|
629
|
-
*
|
|
630
|
-
* The kind has to be in the words. A mark shows one aggregate severity — as a
|
|
631
|
-
* colour and a glyph, neither of which a screen reader receives — so an anchor
|
|
632
|
-
* carrying an error and a warning together would otherwise offer no way to tell
|
|
633
|
-
* which of the two is the one blocking the compile.
|
|
634
|
-
*
|
|
635
|
-
* NEWLINES are the only thing rewritten: each line is trimmed and the lines are
|
|
636
|
-
* run together, so a multi-line host message cannot break the one-problem-per-
|
|
637
|
-
* line reading below. Everything else survives — including internal spacing and
|
|
638
|
-
* the trailing punctuation a diagnostic may be ABOUT ("unexpected ;") — because
|
|
639
|
-
* the canvas has to say what the host's own problem list says. Separators are
|
|
640
|
-
* handled at the join instead ({@link joinSentences}).
|
|
641
|
-
*
|
|
642
|
-
* A problem with no message at all is named by its severity alone, rather than
|
|
643
|
-
* by a colon with nothing after it: nothing in {@link WfProblem} requires the
|
|
644
|
-
* text to be non-empty, and "this step has an error" is still worth saying.
|
|
645
|
-
*/
|
|
646
|
-
function problemSentence(problem) {
|
|
647
|
-
const message = (typeof problem.message === "string" ? problem.message : "").split("\n").map((line) => line.trim()).filter(Boolean).join(" ");
|
|
648
|
-
const word = SEVERITY_WORD[problem.severity] ?? SEVERITY_WORD.error;
|
|
649
|
-
return message ? `${word}: ${message}` : word;
|
|
650
|
-
}
|
|
651
|
-
/** Run sentences together for a listener, adding a full stop only where the text
|
|
652
|
-
* does not already end in one — so "Why?" is followed by a space rather than
|
|
653
|
-
* "Why?.", and a message ending in a semicolon keeps it. Empty in, empty out:
|
|
654
|
-
* `reduce` with no seed throws on an empty list, and this is reachable from any
|
|
655
|
-
* caller holding an empty problem set. */
|
|
656
|
-
function joinSentences(parts) {
|
|
657
|
-
return parts.reduce((joined, part) => joined ? `${joined}${/[.!?;,]$/.test(joined) ? "" : "."} ${part}` : part, "");
|
|
658
|
-
}
|
|
659
|
-
/** Every problem on one anchor, as the tooltip text a marker hangs on. One per
|
|
660
|
-
* line, so a step with three faults lists three rather than concatenating them
|
|
661
|
-
* into a sentence nobody can parse. */
|
|
662
|
-
function problemTitle(problems) {
|
|
663
|
-
return problems.map(problemSentence).join("\n");
|
|
664
|
-
}
|
|
665
|
-
/**
|
|
666
|
-
* Every message on one anchor, as the text a reader who cannot hover receives.
|
|
667
|
-
* The visible mark has room for a glyph and a count, so the messages themselves
|
|
668
|
-
* would otherwise live only in a `title` — which a pointer opens and a keyboard,
|
|
669
|
-
* a screen reader and a touch device do not. Rendering them visually hidden puts
|
|
670
|
-
* the same words in the accessibility tree, where they become the mark's own
|
|
671
|
-
* accessible name.
|
|
672
|
-
*/
|
|
673
|
-
function ProblemMessages({ problems }) {
|
|
674
|
-
if (problems.length === 0) return null;
|
|
675
|
-
return /* @__PURE__ */ jsx("span", {
|
|
676
|
-
className: "sr-only",
|
|
677
|
-
children: problems.length === 1 ? problemSentence(problems[0]) : `${problems.length} problems. ${joinSentences(problems.map(problemSentence))}`
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
/**
|
|
681
|
-
* The mark a node wears when the draft it is drawn from has a problem on it: a
|
|
682
|
-
* warning glyph, with a count once there is more than one. It is a READOUT, not
|
|
683
|
-
* a control — the node's own click already opens whatever the host shows for it,
|
|
684
|
-
* and a second target inside the card would compete with that.
|
|
685
|
-
*
|
|
686
|
-
* It carries every message twice over: on `title`, where a pointer finds it, and
|
|
687
|
-
* as visually hidden text, which is what the accessibility tree reads. That is
|
|
688
|
-
* what makes the mark worth more than the border tint — the canvas says WHICH
|
|
689
|
-
* step is broken at a glance and WHY without a trip to the problem list, however
|
|
690
|
-
* the reader is driving it.
|
|
691
|
-
*/
|
|
692
|
-
function ProblemMarker({ problems, className = "" }) {
|
|
693
|
-
const severity = worstSeverity(problems) ?? "error";
|
|
694
|
-
return /* @__PURE__ */ jsxs("span", {
|
|
695
|
-
"data-testid": "wf-node-problem",
|
|
696
|
-
"data-severity": severity,
|
|
697
|
-
className: `flex shrink-0 items-center gap-0.5 rounded-full border px-1 py-[1px] font-medium text-[10px] leading-none ${className}`,
|
|
698
|
-
style: problemSurface(severity),
|
|
699
|
-
children: [/* @__PURE__ */ jsxs("span", {
|
|
700
|
-
"aria-hidden": true,
|
|
701
|
-
className: "flex items-center gap-0.5",
|
|
702
|
-
title: problemTitle(problems),
|
|
703
|
-
children: [/* @__PURE__ */ jsx(AlertTriangle, { size: 9 }), problems.length > 1 && /* @__PURE__ */ jsx("span", { children: problems.length })]
|
|
704
|
-
}), /* @__PURE__ */ jsx(ProblemMessages, { problems })]
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
/** The status pill in a card's header — the one place the run state is spelled
|
|
708
|
-
* out in words. */
|
|
709
|
-
function StatusPill({ status }) {
|
|
710
|
-
return /* @__PURE__ */ jsx("span", {
|
|
711
|
-
className: "shrink-0 whitespace-nowrap rounded-full border px-2 py-[1px] font-medium text-[10px]",
|
|
712
|
-
style: STATUS_PILL[status],
|
|
713
|
-
children: STATUS_LABEL[status]
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
/**
|
|
717
|
-
* The run status FOOTER: a progress bar (queued = near-empty, running = pulsing
|
|
718
|
-
* partial, terminal = full) over a caption row. Pinned to the card's BOTTOM via
|
|
719
|
-
* `mt-auto` so it lands at the same place on every node regardless of how much
|
|
720
|
-
* content sits above it — unlike an inline strip, whose height drifts. The
|
|
721
|
-
* caption reads agent rounds on the left and elapsed on the right; status itself
|
|
722
|
-
* is carried by the bar color + the header pill, so it is NOT restated here. The
|
|
723
|
-
* caption row always renders (even when empty) so the footer band keeps a
|
|
724
|
-
* constant height across nodes.
|
|
725
|
-
*/
|
|
726
|
-
function StatusFooter({ status, rounds, cost, tokens, elapsed }) {
|
|
727
|
-
const caption = [
|
|
728
|
-
rounds !== void 0 ? `${rounds} round${rounds === 1 ? "" : "s"}` : void 0,
|
|
729
|
-
cost,
|
|
730
|
-
tokens
|
|
731
|
-
].filter(Boolean).join(" · ");
|
|
732
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
733
|
-
className: "wf-node-body-in mt-auto border-border border-t",
|
|
734
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
735
|
-
className: "h-1 w-full overflow-hidden",
|
|
736
|
-
style: { background: MUTED_TRACK },
|
|
737
|
-
children: /* @__PURE__ */ jsx("div", {
|
|
738
|
-
"data-testid": "wf-node-progress",
|
|
739
|
-
className: `h-full ${status === "running" ? "animate-pulse" : ""}`,
|
|
740
|
-
...status === "running" ? { "data-motion": "essential" } : {},
|
|
741
|
-
style: {
|
|
742
|
-
width: progressFill(status),
|
|
743
|
-
background: STATUS_COLOR[status]
|
|
744
|
-
}
|
|
745
|
-
})
|
|
746
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
747
|
-
className: "flex items-center justify-between gap-2 px-3.5 py-1 text-[10px] text-muted-foreground leading-[15px]",
|
|
748
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
749
|
-
className: "min-h-[15px] truncate tabular-nums",
|
|
750
|
-
children: caption
|
|
751
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
752
|
-
className: "min-h-[15px] shrink-0 tabular-nums",
|
|
753
|
-
children: elapsed ?? ""
|
|
754
|
-
})]
|
|
755
|
-
})]
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
/**
|
|
759
|
-
* What an expanded card says where its output WOULD be, when it has none. The
|
|
760
|
-
* card is sized for the output it may yet have (the layout is computed once,
|
|
761
|
-
* before any run state, so it can never reflow mid-run) — leaving that space
|
|
762
|
-
* blank reads as a broken card rather than as a step with nothing to report. A
|
|
763
|
-
* running node is the exception: its output may still be on its way, so it waits
|
|
764
|
-
* quietly rather than claiming there is none.
|
|
765
|
-
*/
|
|
766
|
-
function emptySlotLabel(status) {
|
|
767
|
-
switch (status) {
|
|
768
|
-
case "queued": return "Not run yet";
|
|
769
|
-
case "succeeded": return "No output";
|
|
770
|
-
case "failed": return "No error reported";
|
|
771
|
-
default: return;
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
/**
|
|
775
|
-
* A node's MARK, centered in a tile the caller sizes: the provider's brand logo
|
|
776
|
-
* when the step runs against a connector — the way n8n makes a node recognizable
|
|
777
|
-
* before a word of it is read — else the kind's glyph, tinted by tone. Both
|
|
778
|
-
* densities render the same mark at different sizes, so a node looks like itself
|
|
779
|
-
* whether it's an icon tile or a full card.
|
|
780
|
-
*/
|
|
781
|
-
function NodeMark({ kind, provider, model, accent, tile }) {
|
|
782
|
-
const Icon = kind && KIND_ICON[kind] || Circle;
|
|
783
|
-
const brand = !provider && model ? modelBrandFor(model) : null;
|
|
784
|
-
const mark = Math.round(tile * (provider ? .58 : .5));
|
|
785
|
-
return /* @__PURE__ */ jsx("span", {
|
|
786
|
-
"aria-hidden": "true",
|
|
787
|
-
className: "flex shrink-0 items-center justify-center overflow-hidden rounded-lg border border-border",
|
|
788
|
-
style: {
|
|
789
|
-
width: tile,
|
|
790
|
-
height: tile,
|
|
791
|
-
background: `color-mix(in srgb, ${accent} 10%, hsl(var(--card)))`
|
|
792
|
-
},
|
|
793
|
-
children: provider ? /* @__PURE__ */ jsx(ProviderIcon, {
|
|
794
|
-
id: provider,
|
|
795
|
-
displayName: providerLabel(provider),
|
|
796
|
-
size: mark,
|
|
797
|
-
className: "rounded-[3px]"
|
|
798
|
-
}) : brand ? /* @__PURE__ */ jsx(ModelBrandStack, {
|
|
799
|
-
identity: brand,
|
|
800
|
-
size: brand.combined || tile >= 38 ? "md" : "sm"
|
|
801
|
-
}) : /* @__PURE__ */ jsx(Icon, {
|
|
802
|
-
size: mark,
|
|
803
|
-
strokeWidth: 1.75,
|
|
804
|
-
style: { color: accent }
|
|
805
|
-
})
|
|
806
|
-
});
|
|
807
|
-
}
|
|
808
|
-
//#endregion
|
|
809
290
|
//#region src/workflows/WorkflowGraph.tsx
|
|
810
291
|
/**
|
|
811
292
|
* n8n-style node-graph view of a workflow, rendered from its YAML definition.
|
|
@@ -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 };
|
package/dist/workflows.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { a as modelBrandFor, n as ModelBrandStack, t as ModelBrandIdentity } from "./model-brand-B9A4H9pJ.js";
|
|
2
|
+
import { ReactElement } from "react";
|
|
3
|
+
import { LucideIcon } from "lucide-react";
|
|
2
4
|
import "@xyflow/react";
|
|
3
5
|
import "@xyflow/react/dist/style.css";
|
|
4
6
|
//#region src/workflows/model.d.ts
|
|
@@ -488,4 +490,167 @@ interface WorkflowGraphProps {
|
|
|
488
490
|
//#region src/workflows/WorkflowGraphLazy.d.ts
|
|
489
491
|
declare function WorkflowGraph(props: WorkflowGraphProps): import("react").JSX.Element;
|
|
490
492
|
//#endregion
|
|
491
|
-
|
|
493
|
+
//#region src/workflows/node-ui.d.ts
|
|
494
|
+
export declare const STATUS_COLOR: Record<WfNodeStatus, string>;
|
|
495
|
+
export declare const STATUS_LABEL: Record<WfNodeStatus, string>;
|
|
496
|
+
/** Status pill styling — a tinted well in the status color. Built from the
|
|
497
|
+
* semantic surface trio (bg/border/text) so the pill keeps its contrast in both
|
|
498
|
+
* themes rather than washing out in one of them. */
|
|
499
|
+
export declare const STATUS_PILL: Record<WfNodeStatus, {
|
|
500
|
+
background: string;
|
|
501
|
+
color: string;
|
|
502
|
+
borderColor: string;
|
|
503
|
+
}>;
|
|
504
|
+
export declare const KIND_ICON: Record<string, LucideIcon>;
|
|
505
|
+
/** The status pill in a card's header — the one place the run state is spelled
|
|
506
|
+
* out in words. */
|
|
507
|
+
declare function StatusPill({ status }: {
|
|
508
|
+
status: WfNodeStatus;
|
|
509
|
+
}): import("react").JSX.Element;
|
|
510
|
+
/**
|
|
511
|
+
* A node's MARK, centered in a tile the caller sizes: the provider's brand logo
|
|
512
|
+
* when the step runs against a connector — the way n8n makes a node recognizable
|
|
513
|
+
* before a word of it is read — else the kind's glyph, tinted by tone. Both
|
|
514
|
+
* densities render the same mark at different sizes, so a node looks like itself
|
|
515
|
+
* whether it's an icon tile or a full card.
|
|
516
|
+
*/
|
|
517
|
+
export declare function NodeMark({ kind, provider, model, accent, tile }: {
|
|
518
|
+
kind?: string;
|
|
519
|
+
provider?: string;
|
|
520
|
+
/** The model an agent runs, e.g. "anthropic/claude-sonnet-4-5". Its lab/host
|
|
521
|
+
* brand mark identifies the node far faster than a generic sparkle. */
|
|
522
|
+
model?: string;
|
|
523
|
+
accent: string;
|
|
524
|
+
/** Edge length of the square the mark is centered in. */
|
|
525
|
+
tile: number;
|
|
526
|
+
}): import("react").JSX.Element;
|
|
527
|
+
//#endregion
|
|
528
|
+
//#region src/workflows/naming.d.ts
|
|
529
|
+
/**
|
|
530
|
+
* Human-facing naming for workflow nodes. A definition addresses things by
|
|
531
|
+
* machine identifier — an action kind (`agent.run`), a hub action path
|
|
532
|
+
* (`github.pulls.reviews.create`), a profile slug (`pr-reviewer`) — and showing
|
|
533
|
+
* those verbatim on a node makes the graph read like a config file. These turn
|
|
534
|
+
* an identifier into the label a person would say out loud.
|
|
535
|
+
*
|
|
536
|
+
* Pure and dependency-free so the naming rules are unit-testable without the
|
|
537
|
+
* YAML parser or a render.
|
|
538
|
+
*/
|
|
539
|
+
/** How the first word of a humanized identifier is cased. An initialism is
|
|
540
|
+
* ALWAYS upper-cased whichever is chosen — that is the whole point of the table
|
|
541
|
+
* above, and `.toLowerCase()`ing the result afterwards silently undoes it
|
|
542
|
+
* ("List API keys" → "list api keys"). */
|
|
543
|
+
type LeadCase = "sentence" | "lower" | "title";
|
|
544
|
+
/**
|
|
545
|
+
* Humanize a machine identifier: `pr-reviewer` → "PR reviewer", `sandbox.spawn`
|
|
546
|
+
* → "Sandbox spawn", `postMessage` → "Post message". Known initialisms are
|
|
547
|
+
* upper-cased wherever they appear; `lead` picks the case of the rest.
|
|
548
|
+
*
|
|
549
|
+
* - `sentence` (default): "Create issue comment" — a label or a title.
|
|
550
|
+
* - `lower`: "create issue comment" — a fragment that sits INSIDE a phrase
|
|
551
|
+
* ("On pull request"), where a capital would read as a new sentence. An
|
|
552
|
+
* initialism still wins: `ci_run_completed` → "CI run completed", never
|
|
553
|
+
* "cI run completed".
|
|
554
|
+
* - `title`: "Google Sheets" — a brand/proper name.
|
|
555
|
+
*
|
|
556
|
+
* An identifier that carries no word characters is returned verbatim rather than
|
|
557
|
+
* blanked.
|
|
558
|
+
*/
|
|
559
|
+
export declare function humanizeIdentifier(id: string, lead?: LeadCase): string;
|
|
560
|
+
/** A model slug without its provider prefix (`anthropic/claude-sonnet-5` →
|
|
561
|
+
* `claude-sonnet-5`) — the vendor is noise on a card whose width is the scarce
|
|
562
|
+
* resource, and the full slug stays in the node's config for the detail view. */
|
|
563
|
+
export declare function shortModel(model: string): string;
|
|
564
|
+
/** The parts of a hub action path (`<provider>.<resource…>.<operation>`). */
|
|
565
|
+
interface ActionPathParts {
|
|
566
|
+
/** The connector the action runs against (`github`). */
|
|
567
|
+
provider: string;
|
|
568
|
+
/** The trailing verb (`create`). */
|
|
569
|
+
operation: string;
|
|
570
|
+
/** Everything between provider and operation (`pulls.reviews`), when present —
|
|
571
|
+
* a two-segment path (`slack.postMessage`) has no resource. */
|
|
572
|
+
resource?: string;
|
|
573
|
+
}
|
|
574
|
+
/** Split a hub action path into provider / resource / operation. Null when the
|
|
575
|
+
* value isn't a dotted path (a `${…}` expression, or a bare word), so callers
|
|
576
|
+
* fall back to showing it verbatim instead of inventing structure. */
|
|
577
|
+
export declare function parseActionPath(path: string): ActionPathParts | null;
|
|
578
|
+
/**
|
|
579
|
+
* The operation label for an action path, in n8n's `operation: resource` shape:
|
|
580
|
+
* `github.pulls.reviews.create` → "create: pulls.reviews", `slack.postMessage`
|
|
581
|
+
* → "post message". The provider names the node itself (its title + logo), so
|
|
582
|
+
* it is deliberately absent here. A path we can't split is shown verbatim.
|
|
583
|
+
*/
|
|
584
|
+
export declare function actionPathLabel(path: string): string;
|
|
585
|
+
//#endregion
|
|
586
|
+
//#region src/workflows/node-output.d.ts
|
|
587
|
+
/** The recovered shape of an output preview. */
|
|
588
|
+
type OutputShape = {
|
|
589
|
+
kind: "empty";
|
|
590
|
+
} |
|
|
591
|
+
/** A shallow JSON object, flattened to top-level key/value pairs for a
|
|
592
|
+
* key-value render. `truncated` when more pairs exist than we kept. */
|
|
593
|
+
{
|
|
594
|
+
kind: "json";
|
|
595
|
+
entries: [string, string][];
|
|
596
|
+
truncated: boolean;
|
|
597
|
+
} |
|
|
598
|
+
/** Structured-looking text we won't key/value render — a JSON array, a nested
|
|
599
|
+
* object, or a fragment the host clamped mid-token so it won't parse. Shown in
|
|
600
|
+
* monospace so it still reads as data, not prose. */
|
|
601
|
+
{
|
|
602
|
+
kind: "code";
|
|
603
|
+
text: string;
|
|
604
|
+
} |
|
|
605
|
+
/** Free-form prose (agent output, a status line, an error message). */
|
|
606
|
+
{
|
|
607
|
+
kind: "text";
|
|
608
|
+
text: string;
|
|
609
|
+
};
|
|
610
|
+
/**
|
|
611
|
+
* Flatten an agent's markdown answer into the sentence a person would read out
|
|
612
|
+
* of it. On a two-line card preview the syntax IS the noise: a `## Heading` mid
|
|
613
|
+
* paragraph, a `**bold**` run and a bullet's `-` all survive the clamp while the
|
|
614
|
+
* words that carry the meaning get pushed past it. Stripping the markup (never
|
|
615
|
+
* the words) is what turns the preview back into prose — the full text, markup
|
|
616
|
+
* and all, is still rendered by the node's detail view.
|
|
617
|
+
*
|
|
618
|
+
* Structure-only lines (a fence, a table rule) are dropped; everything else keeps
|
|
619
|
+
* its text and is joined into one flowing line.
|
|
620
|
+
*/
|
|
621
|
+
export declare function condenseText(text: string): string;
|
|
622
|
+
/**
|
|
623
|
+
* Recover the shape of an output preview string. Only text that *begins* like
|
|
624
|
+
* JSON (`{`/`[`) is parse-attempted, so ordinary prose starting with a brace-free
|
|
625
|
+
* sentence is never mis-parsed. A begins-like-JSON string that fails to parse
|
|
626
|
+
* (the host clamped it mid-value) still renders as `code`, not prose, so it keeps
|
|
627
|
+
* its monospace/data affordance.
|
|
628
|
+
*
|
|
629
|
+
* `condenseMarkdown` says the prose IS markdown and may be flattened for the
|
|
630
|
+
* preview ({@link condenseText}). It is OFF by default, and the caller must only
|
|
631
|
+
* turn it on for text an agent actually authored as markdown. Applied to anything
|
|
632
|
+
* else it does not merely look wrong — it silently REWRITES the text: a stack
|
|
633
|
+
* trace's `__init__.py` loses its underscores, and a shell glob `src/**` pairs
|
|
634
|
+
* with a later `**` and both vanish. An error message is the one string a person
|
|
635
|
+
* reads to find out what broke; it must reach them exactly as it was thrown.
|
|
636
|
+
*/
|
|
637
|
+
export declare function classifyOutput(preview: string | undefined, condenseMarkdown?: boolean): OutputShape;
|
|
638
|
+
/**
|
|
639
|
+
* Render a classified output preview's CONTENT (no surrounding frame — each node
|
|
640
|
+
* design supplies its own container/label). `rows` caps the visible lines so the
|
|
641
|
+
* body stays within a card's reserved output space; `tone="error"` colors it for
|
|
642
|
+
* a failure message.
|
|
643
|
+
*
|
|
644
|
+
* `rows="none"` renders the body UNCLAMPED, for a container that scrolls or is
|
|
645
|
+
* sized by its content — a drawer, a detail panel, a selection overlay. It is a
|
|
646
|
+
* word rather than a large number because there is no number that means "do not
|
|
647
|
+
* clamp": a count outside {@link CLAMP_BY_ROWS} falls back to two lines, so
|
|
648
|
+
* `rows={99}` asks for everything and silently shows the least.
|
|
649
|
+
*/
|
|
650
|
+
export declare function NodeOutputBody({ shape, rows, tone }: {
|
|
651
|
+
shape: OutputShape;
|
|
652
|
+
rows?: number | "none";
|
|
653
|
+
tone?: "default" | "error";
|
|
654
|
+
}): ReactElement | null;
|
|
655
|
+
//#endregion
|
|
656
|
+
export { type ActionPathParts, type BuildWorkflowGraphOptions, type LeadCase, type ModelBrandIdentity, ModelBrandStack, StatusPill as NodeStatusPill, type OutputShape, type WfEdge, type WfEdgeKind, type WfEdgeSpec, type WfGraph, type WfNode, type WfNodeData, type WfNodeState, type WfNodeStatus, type WfNodeTone, type WfProblem, type WfProblemSeverity, WorkflowGraph as WorkflowGraphLazy, type WorkflowGraphProps, modelBrandFor };
|
package/dist/workflows.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as modelBrandFor, t as ModelBrandStack } from "./model-brand-hMyDTUqB.js";
|
|
2
|
-
import { a as
|
|
2
|
+
import { A as actionPathLabel, C as branchNodeId, D as triggerNodeIndex, E as triggerNodeId, M as parseActionPath, N as shortModel, S as actionNodeId, a as NodeMark, c as STATUS_COLOR, f as StatusPill, i as KIND_ICON, j as humanizeIdentifier, l as STATUS_LABEL, n as classifyOutput, r as condenseText, t as NodeOutputBody, u as STATUS_PILL, w as buildWorkflowGraph, x as TRIGGER_NODE_ID } from "./node-output-D-fYilAu.js";
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
import { Component, Suspense, lazy, useMemo, useState } from "react";
|
|
5
5
|
//#region src/workflows/WorkflowGraphLazy.tsx
|
|
@@ -30,7 +30,7 @@ function retryImport(factory, retries = 2, delayMs = 350) {
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
function lazyWorkflowGraph() {
|
|
33
|
-
return lazy(() => retryImport(() => import("./WorkflowGraph-
|
|
33
|
+
return lazy(() => retryImport(() => import("./WorkflowGraph-B4mkYGe3.js").then((m) => ({ default: m.WorkflowGraph }))));
|
|
34
34
|
}
|
|
35
35
|
var GraphErrorBoundary = class extends Component {
|
|
36
36
|
state = { hasError: false };
|
|
@@ -81,4 +81,4 @@ function WorkflowGraph(props) {
|
|
|
81
81
|
}, `${props.yaml}::${attempt}`);
|
|
82
82
|
}
|
|
83
83
|
//#endregion
|
|
84
|
-
export { ModelBrandStack, TRIGGER_NODE_ID, WorkflowGraph as WorkflowGraphLazy, actionNodeId, branchNodeId, buildWorkflowGraph, modelBrandFor, triggerNodeId, triggerNodeIndex };
|
|
84
|
+
export { KIND_ICON, ModelBrandStack, NodeMark, NodeOutputBody, StatusPill as NodeStatusPill, STATUS_COLOR, STATUS_LABEL, STATUS_PILL, TRIGGER_NODE_ID, WorkflowGraph as WorkflowGraphLazy, actionNodeId, actionPathLabel, branchNodeId, buildWorkflowGraph, classifyOutput, condenseText, humanizeIdentifier, modelBrandFor, parseActionPath, shortModel, triggerNodeId, triggerNodeIndex };
|
package/package.json
CHANGED