@tangle-network/sandbox-ui 0.47.0 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,295 @@
1
+ import {
2
+ ProviderIcon
3
+ } from "./chunk-GTJ5PSK5.js";
4
+ import {
5
+ buildWorkflowGraph
6
+ } from "./chunk-46VFHJPQ.js";
7
+ import {
8
+ providerLabel
9
+ } from "./chunk-7JLB7UZ3.js";
10
+ import "./chunk-U67V476Y.js";
11
+
12
+ // src/workflows/WorkflowGraph.tsx
13
+ import {
14
+ Background,
15
+ Controls,
16
+ Handle,
17
+ Position,
18
+ ReactFlow
19
+ } from "@xyflow/react";
20
+ import "@xyflow/react/dist/style.css";
21
+ import {
22
+ useCallback,
23
+ useEffect,
24
+ useMemo,
25
+ useState
26
+ } from "react";
27
+
28
+ // src/workflows/flow-graph.ts
29
+ function buildFlowGraph(yaml, nodeState) {
30
+ const graph = buildWorkflowGraph(yaml);
31
+ return {
32
+ error: graph.error,
33
+ nodes: graph.nodes.map(
34
+ (n) => ({
35
+ id: n.id,
36
+ type: "wfNode",
37
+ position: n.position,
38
+ data: nodeState?.[n.id] ? { ...n.data, state: nodeState[n.id] } : n.data
39
+ })
40
+ ),
41
+ edges: graph.edges.map((e) => ({
42
+ id: e.id,
43
+ source: e.source,
44
+ target: e.target,
45
+ sourceHandle: e.sourceHandle,
46
+ type: "smoothstep"
47
+ }))
48
+ };
49
+ }
50
+
51
+ // src/workflows/format.ts
52
+ function fmtDuration(ms) {
53
+ if (ms === void 0 || !Number.isFinite(ms) || ms < 0) return void 0;
54
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
55
+ const s = ms / 1e3;
56
+ if (s < 60) return `${s.toFixed(1)}s`;
57
+ const totalSeconds = Math.round(s);
58
+ return `${Math.floor(totalSeconds / 60)}m${totalSeconds % 60}s`;
59
+ }
60
+ function fmtCost(usd) {
61
+ if (usd === void 0 || !Number.isFinite(usd) || usd < 0) return void 0;
62
+ if (usd === 0) return "$0";
63
+ return usd < 0.01 ? `$${usd.toFixed(4)}` : `$${usd.toFixed(2)}`;
64
+ }
65
+ function clampPreview(text, max = 200) {
66
+ return text.length > max ? `${text.slice(0, max)}\u2026` : text;
67
+ }
68
+ function fmtTokens(input, output) {
69
+ const valid = (n) => n !== void 0 && Number.isFinite(n) && n >= 0 ? n : void 0;
70
+ const i = valid(input);
71
+ const o = valid(output);
72
+ if (i === void 0 && o === void 0) return void 0;
73
+ return `${i ?? 0}/${o ?? 0} tok`;
74
+ }
75
+
76
+ // src/workflows/WorkflowGraph.tsx
77
+ import { jsx, jsxs } from "react/jsx-runtime";
78
+ function useColorMode() {
79
+ const [mode, setMode] = useState(
80
+ () => typeof document !== "undefined" && document.documentElement.classList.contains("dark") ? "dark" : "light"
81
+ );
82
+ useEffect(() => {
83
+ const el = document.documentElement;
84
+ const update = () => setMode(el.classList.contains("dark") ? "dark" : "light");
85
+ update();
86
+ const observer = new MutationObserver(update);
87
+ observer.observe(el, { attributes: true, attributeFilter: ["class"] });
88
+ return () => observer.disconnect();
89
+ }, []);
90
+ return mode;
91
+ }
92
+ var TONE_CARD = {
93
+ trigger: "border-primary/60 bg-primary/5",
94
+ structural: "border-accent-yellow/50 bg-accent-yellow/5",
95
+ action: "border-border bg-card"
96
+ };
97
+ var TONE_DOT = {
98
+ trigger: "bg-primary",
99
+ structural: "bg-accent-yellow",
100
+ action: "bg-text-muted"
101
+ };
102
+ var HANDLE_CLASS = "!h-2 !w-2 !min-w-0 !border-0 !bg-transparent opacity-0";
103
+ var STATUS_LABEL = {
104
+ queued: "Queued",
105
+ running: "Running",
106
+ succeeded: "Done",
107
+ failed: "Failed"
108
+ };
109
+ var STATUS_BADGE = {
110
+ queued: "bg-background-tertiary text-text-muted",
111
+ running: "bg-primary/15 text-primary",
112
+ succeeded: "bg-green-600/10 text-green-600",
113
+ failed: "bg-red-500/10 text-red-400"
114
+ };
115
+ function statusBorder(status) {
116
+ switch (status) {
117
+ case "running":
118
+ return "border-primary ring-2 ring-primary/40 animate-pulse";
119
+ case "succeeded":
120
+ return "border-green-500";
121
+ case "failed":
122
+ return "border-red-500";
123
+ default:
124
+ return "opacity-70";
125
+ }
126
+ }
127
+ function MetaChip({ children }) {
128
+ return /* @__PURE__ */ jsx("span", { className: "rounded bg-background-tertiary px-1.5 py-0.5 text-[10px] text-text-muted", children });
129
+ }
130
+ function WorkflowNode({ data }) {
131
+ const d = data;
132
+ const state = d.state;
133
+ const model = state?.model ?? d.model;
134
+ const duration = fmtDuration(state?.durationMs);
135
+ const cost = fmtCost(state?.costUsd);
136
+ const tokens = fmtTokens(state?.inputTokens, state?.outputTokens);
137
+ const errorText = state?.error ? clampPreview(state.error) : void 0;
138
+ const outputText = state?.outputPreview ? clampPreview(state.outputPreview) : void 0;
139
+ return /* @__PURE__ */ jsxs(
140
+ "div",
141
+ {
142
+ className: `relative w-[240px] rounded-lg border px-3 py-2 shadow-sm transition-colors ${TONE_CARD[d.tone]} ${state ? statusBorder(state.status) : ""}`,
143
+ children: [
144
+ !d.isRoot && /* @__PURE__ */ jsx(
145
+ Handle,
146
+ {
147
+ type: "target",
148
+ position: Position.Top,
149
+ className: HANDLE_CLASS
150
+ }
151
+ ),
152
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
153
+ /* @__PURE__ */ jsx(
154
+ "span",
155
+ {
156
+ "aria-hidden": "true",
157
+ className: `h-2 w-2 shrink-0 rounded-full ${TONE_DOT[d.tone]}`
158
+ }
159
+ ),
160
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-[12px] text-text", children: d.title }),
161
+ state ? /* @__PURE__ */ jsx(
162
+ "span",
163
+ {
164
+ className: `ml-auto rounded px-1.5 py-0.5 text-[10px] ${STATUS_BADGE[state.status]}`,
165
+ children: STATUS_LABEL[state.status]
166
+ }
167
+ ) : d.badge && /* @__PURE__ */ jsx("span", { className: "ml-auto rounded bg-background-tertiary px-1.5 py-0.5 text-[10px] text-text-muted", children: d.badge })
168
+ ] }),
169
+ d.subtitle && /* @__PURE__ */ jsx(
170
+ "p",
171
+ {
172
+ className: "mt-1 truncate text-[11px] text-text-muted",
173
+ title: d.subtitle,
174
+ children: d.subtitle
175
+ }
176
+ ),
177
+ (model || cost || duration || tokens || state) && /* @__PURE__ */ jsxs("div", { className: "mt-1.5 flex flex-wrap items-center gap-1", children: [
178
+ d.kind && d.tone === "action" && /* @__PURE__ */ jsx(MetaChip, { children: d.kind }),
179
+ model && /* @__PURE__ */ jsx(MetaChip, { children: model }),
180
+ cost && /* @__PURE__ */ jsx(MetaChip, { children: cost }),
181
+ duration && /* @__PURE__ */ jsx(MetaChip, { children: duration }),
182
+ tokens && /* @__PURE__ */ jsx(MetaChip, { children: tokens })
183
+ ] }),
184
+ state?.status === "failed" && errorText && /* @__PURE__ */ jsx(
185
+ "p",
186
+ {
187
+ className: "mt-1.5 line-clamp-2 text-[10px] text-red-400",
188
+ title: errorText,
189
+ children: errorText
190
+ }
191
+ ),
192
+ outputText && state?.status !== "failed" && /* @__PURE__ */ jsx(
193
+ "p",
194
+ {
195
+ className: "mt-1.5 line-clamp-2 text-[10px] text-text-muted",
196
+ title: outputText,
197
+ children: outputText
198
+ }
199
+ ),
200
+ d.provider && /* @__PURE__ */ jsxs("span", { className: "mt-1.5 inline-flex items-center gap-1 rounded bg-background-tertiary px-1.5 py-0.5 text-[10px] text-text-muted", children: [
201
+ /* @__PURE__ */ jsx(
202
+ ProviderIcon,
203
+ {
204
+ id: d.provider,
205
+ displayName: providerLabel(d.provider),
206
+ size: 12,
207
+ className: "rounded-[2px]"
208
+ }
209
+ ),
210
+ providerLabel(d.provider)
211
+ ] }),
212
+ /* @__PURE__ */ jsx(
213
+ Handle,
214
+ {
215
+ type: "source",
216
+ id: "out",
217
+ position: Position.Bottom,
218
+ className: HANDLE_CLASS
219
+ }
220
+ ),
221
+ d.hasBranches && /* @__PURE__ */ jsx(
222
+ Handle,
223
+ {
224
+ type: "source",
225
+ id: "branch",
226
+ position: Position.Right,
227
+ className: HANDLE_CLASS
228
+ }
229
+ )
230
+ ]
231
+ }
232
+ );
233
+ }
234
+ var NODE_TYPES = { wfNode: WorkflowNode };
235
+ function WorkflowGraph({
236
+ yaml,
237
+ variant = "full",
238
+ className,
239
+ nodeState,
240
+ onNodeClick
241
+ }) {
242
+ const colorMode = useColorMode();
243
+ const { nodes, edges, error } = useMemo(
244
+ () => buildFlowGraph(yaml, nodeState),
245
+ [yaml, nodeState]
246
+ );
247
+ const handleNodeClick = useCallback(
248
+ (_event, node) => {
249
+ onNodeClick?.(node.id, node.data);
250
+ },
251
+ [onNodeClick]
252
+ );
253
+ if (error || nodes.length === 0) {
254
+ return /* @__PURE__ */ jsx(
255
+ "div",
256
+ {
257
+ className: `flex items-center justify-center rounded-lg border border-border border-dashed p-4 text-center text-text-muted text-xs ${className ?? ""}`,
258
+ children: error ?? "Nothing to show"
259
+ }
260
+ );
261
+ }
262
+ const isPreview = variant === "preview";
263
+ return /* @__PURE__ */ jsx("div", { className: `wf-graph ${className ?? ""}`, children: /* @__PURE__ */ jsxs(
264
+ ReactFlow,
265
+ {
266
+ nodes,
267
+ edges,
268
+ nodeTypes: NODE_TYPES,
269
+ colorMode,
270
+ onNodeClick: onNodeClick ? handleNodeClick : void 0,
271
+ fitView: true,
272
+ fitViewOptions: { padding: 0.2 },
273
+ proOptions: { hideAttribution: true },
274
+ nodesDraggable: !isPreview,
275
+ nodesConnectable: false,
276
+ elementsSelectable: !isPreview,
277
+ edgesFocusable: false,
278
+ zoomOnScroll: false,
279
+ zoomOnPinch: true,
280
+ zoomOnDoubleClick: !isPreview,
281
+ panOnDrag: true,
282
+ preventScrolling: false,
283
+ minZoom: 0.2,
284
+ maxZoom: 1.5,
285
+ children: [
286
+ /* @__PURE__ */ jsx(Background, { gap: 16 }),
287
+ /* @__PURE__ */ jsx(Controls, { showInteractive: false, position: "bottom-right" })
288
+ ]
289
+ }
290
+ ) });
291
+ }
292
+ export {
293
+ WorkflowGraph,
294
+ WorkflowNode
295
+ };
@@ -1899,6 +1899,7 @@ function SidebarLayoutInner({
1899
1899
  profileMenuItems,
1900
1900
  showThemeToggle = false,
1901
1901
  railFooter,
1902
+ railCollapseToggle = "footer",
1902
1903
  LinkComponent,
1903
1904
  hideBelow,
1904
1905
  railLabels = false,
@@ -1915,15 +1916,20 @@ function SidebarLayoutInner({
1915
1916
  return /* @__PURE__ */ jsxs14("div", { className: cn("min-h-screen bg-surface text-foreground", className), children: [
1916
1917
  /* @__PURE__ */ jsx15("div", { className: cn(hideBelow && HIDE_BELOW_CLASS[hideBelow]), children: /* @__PURE__ */ jsxs14(Sidebar, { className: sidebarClassName, children: [
1917
1918
  /* @__PURE__ */ jsxs14(SidebarRail, { children: [
1918
- (railHeaderContent !== void 0 || logo !== void 0) && /* @__PURE__ */ jsx15(SidebarRailHeader, { className: cn(showLabels && (railHeaderContent !== void 0 ? "px-2" : "justify-start px-4")), children: railHeaderContent !== void 0 ? railHeaderContent : /* @__PURE__ */ jsx15(
1919
- Link,
1920
- {
1921
- href: logoHref,
1922
- to: logoHref,
1923
- className: "flex items-center justify-center rounded-lg p-1 transition-colors hover:bg-surface-container-high",
1924
- children: logo
1925
- }
1926
- ) }),
1919
+ (railHeaderContent !== void 0 || logo !== void 0 || railLabels && railCollapseToggle === "header") && /* @__PURE__ */ jsxs14(SidebarRailHeader, { className: cn(showLabels && (railHeaderContent !== void 0 ? "px-2" : "justify-start px-4")), children: [
1920
+ railHeaderContent !== void 0 ? railHeaderContent : logo !== void 0 ? /* @__PURE__ */ jsx15(
1921
+ Link,
1922
+ {
1923
+ href: logoHref,
1924
+ to: logoHref,
1925
+ className: "flex items-center justify-center rounded-lg p-1 transition-colors hover:bg-surface-container-high",
1926
+ children: logo
1927
+ }
1928
+ ) : null,
1929
+ railLabels && railCollapseToggle === "header" && // Icon-only in the header row (the labeled, full-width variant is
1930
+ // for the vertical footer stack); sits beside the logo/content.
1931
+ /* @__PURE__ */ jsx15(RailCollapseToggle, { collapsed: railCollapsed, showLabel: false, onToggle: toggleRail, className: "shrink-0" })
1932
+ ] }),
1927
1933
  /* @__PURE__ */ jsx15(SidebarRailNav, { className: showLabels ? "px-2" : void 0, children: navItems.map((item) => {
1928
1934
  if (item.flyoutItems && item.flyoutItems.length > 0) {
1929
1935
  const activeSet = new Set(item.flyoutActiveIds ?? []);
@@ -1983,7 +1989,7 @@ function SidebarLayoutInner({
1983
1989
  ) }, item.id);
1984
1990
  }) }),
1985
1991
  (railLabels || railFooter !== void 0 || hasProfile) && /* @__PURE__ */ jsxs14(SidebarRailFooter, { className: cn("border-t border-[var(--md3-outline-variant)] pt-2", showLabels && "items-stretch px-2"), children: [
1986
- railLabels && /* @__PURE__ */ jsx15(RailCollapseToggle, { collapsed: railCollapsed, showLabel: showLabels, onToggle: toggleRail }),
1992
+ railLabels && railCollapseToggle === "footer" && /* @__PURE__ */ jsx15(RailCollapseToggle, { collapsed: railCollapsed, showLabel: showLabels, onToggle: toggleRail }),
1987
1993
  railFooter,
1988
1994
  hasProfile && (() => {
1989
1995
  const profile = /* @__PURE__ */ jsx15(
@@ -21,6 +21,16 @@ function urlHost(url) {
21
21
  return url;
22
22
  }
23
23
  }
24
+ function pickDetail(cfg, keys) {
25
+ const out = {};
26
+ for (const key of keys) {
27
+ const v = cfg[key];
28
+ if (v === void 0 || v === null) continue;
29
+ const text = typeof v === "string" ? v : typeof v === "number" || typeof v === "boolean" ? String(v) : Array.isArray(v) ? `${v.length} item${v.length === 1 ? "" : "s"}` : "\u2026";
30
+ out[key] = text.length > 200 ? `${text.slice(0, 200)}\u2026` : text;
31
+ }
32
+ return Object.keys(out).length > 0 ? out : void 0;
33
+ }
24
34
  function describeAction(action) {
25
35
  const rec = asRecord(action);
26
36
  const [kind] = Object.keys(rec);
@@ -29,7 +39,9 @@ function describeAction(action) {
29
39
  case "sandbox.spawn":
30
40
  return {
31
41
  title: "Spawn sandbox",
42
+ kind,
32
43
  subtitle: str(cfg.template),
44
+ detail: pickDetail(cfg, ["template", "size", "region"]),
33
45
  hasBranches: false,
34
46
  isRoot: false,
35
47
  tone: "action"
@@ -38,8 +50,11 @@ function describeAction(action) {
38
50
  const path = str(cfg.path);
39
51
  return {
40
52
  title: "Integration",
53
+ kind,
41
54
  subtitle: path,
55
+ path,
42
56
  provider: path?.split(".")[0],
57
+ detail: pickDetail(cfg, ["path"]),
43
58
  hasBranches: false,
44
59
  isRoot: false,
45
60
  tone: "action"
@@ -49,7 +64,9 @@ function describeAction(action) {
49
64
  const url = str(cfg.url);
50
65
  return {
51
66
  title: "Notify",
67
+ kind,
52
68
  subtitle: url ? urlHost(url) : void 0,
69
+ detail: pickDetail(cfg, ["url"]),
53
70
  hasBranches: false,
54
71
  isRoot: false,
55
72
  tone: "action"
@@ -58,7 +75,16 @@ function describeAction(action) {
58
75
  case "agent.run":
59
76
  return {
60
77
  title: "Run agent",
78
+ kind,
61
79
  subtitle: str(cfg.model) ?? str(cfg.profile) ?? str(cfg.prompt),
80
+ model: str(cfg.model),
81
+ detail: pickDetail(cfg, [
82
+ "profile",
83
+ "model",
84
+ "maxRounds",
85
+ "size",
86
+ "prompt"
87
+ ]),
62
88
  hasBranches: false,
63
89
  isRoot: false,
64
90
  tone: "action"
@@ -67,6 +93,7 @@ function describeAction(action) {
67
93
  const branches = Array.isArray(cfg.branches) ? cfg.branches : [];
68
94
  return {
69
95
  title: "Parallel",
96
+ kind,
70
97
  subtitle: `${branches.length} branch${branches.length === 1 ? "" : "es"}`,
71
98
  badge: branches.length > 0 ? `\xD7${branches.length}` : void 0,
72
99
  hasBranches: branches.length > 0,
@@ -77,7 +104,9 @@ function describeAction(action) {
77
104
  case "foreach":
78
105
  return {
79
106
  title: "For each",
107
+ kind,
80
108
  subtitle: typeof cfg.items === "string" ? cfg.items : "list",
109
+ detail: pickDetail(cfg, ["items"]),
81
110
  // Only fans out when a `do` template is actually present.
82
111
  hasBranches: Boolean(cfg.do),
83
112
  isRoot: false,
@@ -86,6 +115,7 @@ function describeAction(action) {
86
115
  default:
87
116
  return {
88
117
  title: kind ? kind : "Action",
118
+ kind: kind ?? "action",
89
119
  hasBranches: false,
90
120
  isRoot: false,
91
121
  tone: "action"
@@ -106,6 +136,7 @@ function describeTrigger(on) {
106
136
  if (repo) subtitle += ` on ${repo}`;
107
137
  return {
108
138
  title: "Trigger",
139
+ kind: "provider_event",
109
140
  subtitle,
110
141
  provider: connection,
111
142
  hasBranches: false,
@@ -119,6 +150,7 @@ function describeTrigger(on) {
119
150
  const tz = str(sch.timezone);
120
151
  return {
121
152
  title: "Schedule",
153
+ kind: "schedule",
122
154
  subtitle: cron ? tz ? `${cron} (${tz})` : cron : void 0,
123
155
  hasBranches: false,
124
156
  isRoot: true,
@@ -127,6 +159,7 @@ function describeTrigger(on) {
127
159
  }
128
160
  return {
129
161
  title: "Trigger",
162
+ kind: "trigger",
130
163
  hasBranches: false,
131
164
  isRoot: true,
132
165
  tone: "trigger"
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-3J6FG3FJ.js";
4
4
  import {
5
5
  PortsList
6
- } from "./chunk-PZ62T5PM.js";
6
+ } from "./chunk-3WUSPCPC.js";
7
7
  import {
8
8
  TerminalView
9
9
  } from "./chunk-VIREDUBF.js";
@@ -806,6 +806,13 @@ interface SidebarLayoutProps {
806
806
  showThemeToggle?: boolean;
807
807
  /** Extra content in the rail footer, above the profile avatar. */
808
808
  railFooter?: React.ReactNode;
809
+ /**
810
+ * Where the rail collapse/expand control sits. Defaults to `"footer"` (the
811
+ * historical placement, above the profile avatar). Set `"header"` to move it
812
+ * to the top of the rail instead, alongside the logo/`railHeaderContent`.
813
+ * Only takes effect when `railLabels` is set (the rail is collapsible).
814
+ */
815
+ railCollapseToggle?: "header" | "footer";
809
816
  LinkComponent?: React.ComponentType<any>;
810
817
  /**
811
818
  * Controlled panel-open state. Provide it (seeded from a server-readable
package/dist/dashboard.js CHANGED
@@ -52,7 +52,7 @@ import {
52
52
  canAdminSandbox,
53
53
  parseInsufficientBalance,
54
54
  useSidebar
55
- } from "./chunk-PZ62T5PM.js";
55
+ } from "./chunk-3WUSPCPC.js";
56
56
  import {
57
57
  BillingDashboard,
58
58
  InfoPanel,
package/dist/globals.css CHANGED
@@ -1915,6 +1915,9 @@
1915
1915
  .border-foreground {
1916
1916
  border-color: var(--color-foreground);
1917
1917
  }
1918
+ .border-green-500 {
1919
+ border-color: var(--color-green-500);
1920
+ }
1918
1921
  .border-green-500\/30 {
1919
1922
  border-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 30%, transparent);
1920
1923
  @supports (color: color-mix(in lab, red, red)) {
@@ -1960,6 +1963,9 @@
1960
1963
  border-color: color-mix(in oklab, var(--color-primary) 60%, transparent);
1961
1964
  }
1962
1965
  }
1966
+ .border-red-500 {
1967
+ border-color: var(--color-red-500);
1968
+ }
1963
1969
  .border-red-500\/20 {
1964
1970
  border-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 20%, transparent);
1965
1971
  @supports (color: color-mix(in lab, red, red)) {
@@ -2275,6 +2281,12 @@
2275
2281
  .bg-green-500 {
2276
2282
  background-color: var(--color-green-500);
2277
2283
  }
2284
+ .bg-green-600\/10 {
2285
+ background-color: color-mix(in srgb, oklch(62.7% 0.194 149.214) 10%, transparent);
2286
+ @supports (color: color-mix(in lab, red, red)) {
2287
+ background-color: color-mix(in oklab, var(--color-green-600) 10%, transparent);
2288
+ }
2289
+ }
2278
2290
  .bg-green-600\/20 {
2279
2291
  background-color: color-mix(in srgb, oklch(62.7% 0.194 149.214) 20%, transparent);
2280
2292
  @supports (color: color-mix(in lab, red, red)) {
@@ -3202,6 +3214,9 @@
3202
3214
  .text-green-400 {
3203
3215
  color: var(--color-green-400);
3204
3216
  }
3217
+ .text-green-600 {
3218
+ color: var(--color-green-600);
3219
+ }
3205
3220
  .text-muted-foreground {
3206
3221
  color: var(--color-muted-foreground);
3207
3222
  }
@@ -3455,6 +3470,12 @@
3455
3470
  --tw-ring-color: color-mix(in oklab, var(--color-primary) 25%, transparent);
3456
3471
  }
3457
3472
  }
3473
+ .ring-primary\/40 {
3474
+ --tw-ring-color: color-mix(in srgb, hsl(var(--primary)) 40%, transparent);
3475
+ @supports (color: color-mix(in lab, red, red)) {
3476
+ --tw-ring-color: color-mix(in oklab, var(--color-primary) 40%, transparent);
3477
+ }
3478
+ }
3458
3479
  .ring-offset-background {
3459
3480
  --tw-ring-offset-color: var(--color-background);
3460
3481
  }
package/dist/index.js CHANGED
@@ -148,7 +148,7 @@ import {
148
148
  buildUnifiedPatch,
149
149
  computeDiffStats,
150
150
  resolveLanguage
151
- } from "./chunk-NXUSLH7H.js";
151
+ } from "./chunk-4HLWA2VN.js";
152
152
  import {
153
153
  FileArtifactPane,
154
154
  FilePreview,
@@ -211,7 +211,7 @@ import {
211
211
  canAdminSandbox,
212
212
  parseInsufficientBalance,
213
213
  useSidebar
214
- } from "./chunk-PZ62T5PM.js";
214
+ } from "./chunk-3WUSPCPC.js";
215
215
  import {
216
216
  BillingDashboard,
217
217
  InfoPanel,
package/dist/styles.css CHANGED
@@ -1915,6 +1915,9 @@
1915
1915
  .border-foreground {
1916
1916
  border-color: var(--color-foreground);
1917
1917
  }
1918
+ .border-green-500 {
1919
+ border-color: var(--color-green-500);
1920
+ }
1918
1921
  .border-green-500\/30 {
1919
1922
  border-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 30%, transparent);
1920
1923
  @supports (color: color-mix(in lab, red, red)) {
@@ -1960,6 +1963,9 @@
1960
1963
  border-color: color-mix(in oklab, var(--color-primary) 60%, transparent);
1961
1964
  }
1962
1965
  }
1966
+ .border-red-500 {
1967
+ border-color: var(--color-red-500);
1968
+ }
1963
1969
  .border-red-500\/20 {
1964
1970
  border-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 20%, transparent);
1965
1971
  @supports (color: color-mix(in lab, red, red)) {
@@ -2275,6 +2281,12 @@
2275
2281
  .bg-green-500 {
2276
2282
  background-color: var(--color-green-500);
2277
2283
  }
2284
+ .bg-green-600\/10 {
2285
+ background-color: color-mix(in srgb, oklch(62.7% 0.194 149.214) 10%, transparent);
2286
+ @supports (color: color-mix(in lab, red, red)) {
2287
+ background-color: color-mix(in oklab, var(--color-green-600) 10%, transparent);
2288
+ }
2289
+ }
2278
2290
  .bg-green-600\/20 {
2279
2291
  background-color: color-mix(in srgb, oklch(62.7% 0.194 149.214) 20%, transparent);
2280
2292
  @supports (color: color-mix(in lab, red, red)) {
@@ -3202,6 +3214,9 @@
3202
3214
  .text-green-400 {
3203
3215
  color: var(--color-green-400);
3204
3216
  }
3217
+ .text-green-600 {
3218
+ color: var(--color-green-600);
3219
+ }
3205
3220
  .text-muted-foreground {
3206
3221
  color: var(--color-muted-foreground);
3207
3222
  }
@@ -3455,6 +3470,12 @@
3455
3470
  --tw-ring-color: color-mix(in oklab, var(--color-primary) 25%, transparent);
3456
3471
  }
3457
3472
  }
3473
+ .ring-primary\/40 {
3474
+ --tw-ring-color: color-mix(in srgb, hsl(var(--primary)) 40%, transparent);
3475
+ @supports (color: color-mix(in lab, red, red)) {
3476
+ --tw-ring-color: color-mix(in oklab, var(--color-primary) 40%, transparent);
3477
+ }
3478
+ }
3458
3479
  .ring-offset-background {
3459
3480
  --tw-ring-offset-color: var(--color-background);
3460
3481
  }
package/dist/workbench.js CHANGED
@@ -12,9 +12,9 @@ import {
12
12
  buildUnifiedPatch,
13
13
  computeDiffStats,
14
14
  resolveLanguage
15
- } from "./chunk-NXUSLH7H.js";
15
+ } from "./chunk-4HLWA2VN.js";
16
16
  import "./chunk-3J6FG3FJ.js";
17
- import "./chunk-PZ62T5PM.js";
17
+ import "./chunk-3WUSPCPC.js";
18
18
  import "./chunk-OMUIPJQZ.js";
19
19
  import "./chunk-VIREDUBF.js";
20
20
  import "./chunk-77WVVJA4.js";
@@ -12,11 +12,46 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
12
12
  * so the spine stays a single readable column.
13
13
  */
14
14
  type WfNodeTone = "trigger" | "structural" | "action";
15
+ type WfNodeStatus = "queued" | "running" | "succeeded" | "failed";
16
+ /**
17
+ * Live RUN state for one node, supplied by the host (keyed by node id) and
18
+ * merged onto {@link WfNodeData.state} at render time. Absent ⇒ the graph shows
19
+ * the static definition only (the assistant-proposal preview path). Driven from
20
+ * a workflow run's per-action results so the graph animates as the run executes.
21
+ */
22
+ interface WfNodeState {
23
+ status: WfNodeStatus;
24
+ /** Amount booked for this action, USD. */
25
+ costUsd?: number;
26
+ /** Wall-clock the action took, ms. */
27
+ durationMs?: number;
28
+ /** Model the action actually used (may differ from the requested model). */
29
+ model?: string;
30
+ /** Failure message, when the node failed. */
31
+ error?: string;
32
+ /** Short preview of the action's output (or a failure's partial text). */
33
+ outputPreview?: string;
34
+ /** Input/output token usage, when known. */
35
+ inputTokens?: number;
36
+ outputTokens?: number;
37
+ }
15
38
  interface WfNodeData extends Record<string, unknown> {
16
39
  /** Headline for the node, e.g. "Run agent". */
17
40
  title: string;
41
+ /** The action/trigger kind verbatim, e.g. "agent.run", "schedule". Set on every
42
+ * node `buildWorkflowGraph` produces so a card can label what it is regardless
43
+ * of which subtitle it shows; optional on the type so external consumers
44
+ * constructing `WfNodeData` directly aren't forced to supply it (the render
45
+ * guards its usage). */
46
+ kind?: string;
18
47
  /** Secondary detail, e.g. an integration path or a cron expression. */
19
48
  subtitle?: string;
49
+ /** Requested model (agent.run), shown as a chip even before a run. */
50
+ model?: string;
51
+ /** `integration.invoke` provider.method path. */
52
+ path?: string;
53
+ /** Notable config fields for the expand drawer (kept small + stringified). */
54
+ detail?: Record<string, string>;
20
55
  /** Connector slug (e.g. `github`) for the provider chip, when one applies. */
21
56
  provider?: string;
22
57
  /** Small corner tag, e.g. "×3" for a parallel fan-out. */
@@ -26,6 +61,8 @@ interface WfNodeData extends Record<string, unknown> {
26
61
  /** Whether this node is the spine root (no incoming/target handle). */
27
62
  isRoot: boolean;
28
63
  tone: WfNodeTone;
64
+ /** Live run state, merged in by the host at render time (never from YAML). */
65
+ state?: WfNodeState;
29
66
  }
30
67
  interface WfNode {
31
68
  id: string;
@@ -60,8 +97,23 @@ interface WorkflowGraphProps {
60
97
  variant?: "full" | "preview";
61
98
  /** Sizing for the wrapper; the caller controls height. */
62
99
  className?: string;
100
+ /**
101
+ * Live per-node run state, keyed by graph node id (`trigger`, `a0`, `a0-b1`).
102
+ * Absent ⇒ the static definition view (the proposal-card preview passes
103
+ * nothing). When present, each node shows its status/cost/duration/output and
104
+ * the running node pulses.
105
+ *
106
+ * Immutability contract: the node memo keys on this object's reference, so the
107
+ * host MUST pass a NEW top-level `nodeState` object whenever any node's state
108
+ * changes (mutating a nested entry in place will not re-render). Building a
109
+ * fresh record each update — e.g. from a poll/SSE tick — satisfies this.
110
+ */
111
+ nodeState?: Record<string, WfNodeState>;
112
+ /** Click handler for a node (e.g. open a detail drawer). Absent ⇒ nodes are
113
+ * non-interactive on click. */
114
+ onNodeClick?: (nodeId: string, data: WfNodeData) => void;
63
115
  }
64
116
 
65
117
  declare function WorkflowGraph(props: WorkflowGraphProps): react_jsx_runtime.JSX.Element;
66
118
 
67
- export { type WfEdge, type WfGraph, type WfNode, type WfNodeData, type WfNodeTone, WorkflowGraph as WorkflowGraphLazy, type WorkflowGraphProps, buildWorkflowGraph };
119
+ export { type WfEdge, type WfGraph, type WfNode, type WfNodeData, type WfNodeState, type WfNodeStatus, type WfNodeTone, WorkflowGraph as WorkflowGraphLazy, type WorkflowGraphProps, buildWorkflowGraph };
package/dist/workflows.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  buildWorkflowGraph
3
- } from "./chunk-SUC6ZJZW.js";
3
+ } from "./chunk-46VFHJPQ.js";
4
4
  import "./chunk-7JLB7UZ3.js";
5
5
  import "./chunk-U67V476Y.js";
6
6
 
@@ -27,7 +27,7 @@ function retryImport(factory, retries = 2, delayMs = 350) {
27
27
  function lazyWorkflowGraph() {
28
28
  return lazy(
29
29
  () => retryImport(
30
- () => import("./WorkflowGraph-EOV2P5AM.js").then((m) => ({ default: m.WorkflowGraph }))
30
+ () => import("./WorkflowGraph-4JREVTSK.js").then((m) => ({ default: m.WorkflowGraph }))
31
31
  )
32
32
  );
33
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/sandbox-ui",
3
- "version": "0.47.0",
3
+ "version": "0.49.0",
4
4
  "description": "Unified UI component library for Tangle Sandbox — primitives, chat, dashboard, terminal, editor, and workspace components",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,184 +0,0 @@
1
- import {
2
- ProviderIcon
3
- } from "./chunk-GTJ5PSK5.js";
4
- import {
5
- buildWorkflowGraph
6
- } from "./chunk-SUC6ZJZW.js";
7
- import {
8
- providerLabel
9
- } from "./chunk-7JLB7UZ3.js";
10
- import "./chunk-U67V476Y.js";
11
-
12
- // src/workflows/WorkflowGraph.tsx
13
- import {
14
- Background,
15
- Controls,
16
- Handle,
17
- Position,
18
- ReactFlow
19
- } from "@xyflow/react";
20
- import "@xyflow/react/dist/style.css";
21
- import { useEffect, useMemo, useState } from "react";
22
- import { jsx, jsxs } from "react/jsx-runtime";
23
- function useColorMode() {
24
- const [mode, setMode] = useState(
25
- () => typeof document !== "undefined" && document.documentElement.classList.contains("dark") ? "dark" : "light"
26
- );
27
- useEffect(() => {
28
- const el = document.documentElement;
29
- const update = () => setMode(el.classList.contains("dark") ? "dark" : "light");
30
- update();
31
- const observer = new MutationObserver(update);
32
- observer.observe(el, { attributes: true, attributeFilter: ["class"] });
33
- return () => observer.disconnect();
34
- }, []);
35
- return mode;
36
- }
37
- var TONE_CARD = {
38
- trigger: "border-primary/60 bg-primary/5",
39
- structural: "border-accent-yellow/50 bg-accent-yellow/5",
40
- action: "border-border bg-card"
41
- };
42
- var TONE_DOT = {
43
- trigger: "bg-primary",
44
- structural: "bg-accent-yellow",
45
- action: "bg-text-muted"
46
- };
47
- var HANDLE_CLASS = "!h-2 !w-2 !min-w-0 !border-0 !bg-transparent opacity-0";
48
- function WorkflowNode({ data }) {
49
- const d = data;
50
- return /* @__PURE__ */ jsxs(
51
- "div",
52
- {
53
- className: `relative w-[240px] rounded-lg border px-3 py-2 shadow-sm ${TONE_CARD[d.tone]}`,
54
- children: [
55
- !d.isRoot && /* @__PURE__ */ jsx(
56
- Handle,
57
- {
58
- type: "target",
59
- position: Position.Top,
60
- className: HANDLE_CLASS
61
- }
62
- ),
63
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
64
- /* @__PURE__ */ jsx(
65
- "span",
66
- {
67
- "aria-hidden": "true",
68
- className: `h-2 w-2 shrink-0 rounded-full ${TONE_DOT[d.tone]}`
69
- }
70
- ),
71
- /* @__PURE__ */ jsx("span", { className: "font-medium text-[12px] text-text", children: d.title }),
72
- d.badge && /* @__PURE__ */ jsx("span", { className: "ml-auto rounded bg-background-tertiary px-1.5 py-0.5 text-[10px] text-text-muted", children: d.badge })
73
- ] }),
74
- d.subtitle && /* @__PURE__ */ jsx(
75
- "p",
76
- {
77
- className: "mt-1 truncate text-[11px] text-text-muted",
78
- title: d.subtitle,
79
- children: d.subtitle
80
- }
81
- ),
82
- d.provider && /* @__PURE__ */ jsxs("span", { className: "mt-1.5 inline-flex items-center gap-1 rounded bg-background-tertiary px-1.5 py-0.5 text-[10px] text-text-muted", children: [
83
- /* @__PURE__ */ jsx(
84
- ProviderIcon,
85
- {
86
- id: d.provider,
87
- displayName: providerLabel(d.provider),
88
- size: 12,
89
- className: "rounded-[2px]"
90
- }
91
- ),
92
- providerLabel(d.provider)
93
- ] }),
94
- /* @__PURE__ */ jsx(
95
- Handle,
96
- {
97
- type: "source",
98
- id: "out",
99
- position: Position.Bottom,
100
- className: HANDLE_CLASS
101
- }
102
- ),
103
- d.hasBranches && /* @__PURE__ */ jsx(
104
- Handle,
105
- {
106
- type: "source",
107
- id: "branch",
108
- position: Position.Right,
109
- className: HANDLE_CLASS
110
- }
111
- )
112
- ]
113
- }
114
- );
115
- }
116
- var NODE_TYPES = { wfNode: WorkflowNode };
117
- function WorkflowGraph({
118
- yaml,
119
- variant = "full",
120
- className
121
- }) {
122
- const colorMode = useColorMode();
123
- const { nodes, edges, error } = useMemo(() => {
124
- const graph = buildWorkflowGraph(yaml);
125
- return {
126
- error: graph.error,
127
- nodes: graph.nodes.map(
128
- (n) => ({
129
- id: n.id,
130
- type: "wfNode",
131
- position: n.position,
132
- data: n.data
133
- })
134
- ),
135
- edges: graph.edges.map((e) => ({
136
- id: e.id,
137
- source: e.source,
138
- target: e.target,
139
- sourceHandle: e.sourceHandle,
140
- type: "smoothstep"
141
- }))
142
- };
143
- }, [yaml]);
144
- if (error || nodes.length === 0) {
145
- return /* @__PURE__ */ jsx(
146
- "div",
147
- {
148
- className: `flex items-center justify-center rounded-lg border border-border border-dashed p-4 text-center text-text-muted text-xs ${className ?? ""}`,
149
- children: error ?? "Nothing to show"
150
- }
151
- );
152
- }
153
- const isPreview = variant === "preview";
154
- return /* @__PURE__ */ jsx("div", { className: `wf-graph ${className ?? ""}`, children: /* @__PURE__ */ jsxs(
155
- ReactFlow,
156
- {
157
- nodes,
158
- edges,
159
- nodeTypes: NODE_TYPES,
160
- colorMode,
161
- fitView: true,
162
- fitViewOptions: { padding: 0.2 },
163
- proOptions: { hideAttribution: true },
164
- nodesDraggable: !isPreview,
165
- nodesConnectable: false,
166
- elementsSelectable: !isPreview,
167
- edgesFocusable: false,
168
- zoomOnScroll: false,
169
- zoomOnPinch: true,
170
- zoomOnDoubleClick: !isPreview,
171
- panOnDrag: true,
172
- preventScrolling: false,
173
- minZoom: 0.2,
174
- maxZoom: 1.5,
175
- children: [
176
- /* @__PURE__ */ jsx(Background, { gap: 16 }),
177
- /* @__PURE__ */ jsx(Controls, { showInteractive: false, position: "bottom-right" })
178
- ]
179
- }
180
- ) });
181
- }
182
- export {
183
- WorkflowGraph
184
- };