jev-devtools 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,18 +6,46 @@ import { JsonBlock } from "./TraceContent.js";
6
6
  import { conditionInputs, conditionRequirement, } from "./condition-inputs.js";
7
7
  import { formatFlowCondition } from "./flow-condition.js";
8
8
  import { TraceDiagram } from "./TraceDiagram.js";
9
+ import { FlowDetailsPanel } from "./FlowDetailsPanel.js";
10
+ import { DiagramContent } from "./DiagramContent.js";
11
+ // Measure in canvas coordinates: screen rectangles also include the camera zoom.
12
+ function useNodeHeight(id, onResize) {
13
+ const ref = useRef(null);
14
+ useEffect(() => {
15
+ const element = ref.current;
16
+ if (!element)
17
+ return;
18
+ let previous = 0;
19
+ const measure = () => {
20
+ const height = element.offsetHeight;
21
+ // A closed drawer can hide the flow without unmounting it.
22
+ if (height > 0 && height !== previous) {
23
+ previous = height;
24
+ onResize(id, height);
25
+ }
26
+ };
27
+ measure();
28
+ const observer = new ResizeObserver(measure);
29
+ observer.observe(element);
30
+ return () => observer.disconnect();
31
+ }, [id, onResize]);
32
+ return ref;
33
+ }
9
34
  const RequestNode = memo(function RequestNode({ node, trace, status, selected, x, y, onSelect, onResize, }) {
10
- const headingHeight = node.description ? 104 : 64;
11
- const measure = useCallback((height) => onResize(node.id, height + headingHeight), [node.id, onResize, headingHeight]);
12
- return (_jsxs("article", { className: `flow-step flow-request ${selected ? "selected" : ""}`, "data-status": status, "aria-label": `Request ${node.id}`, style: { left: x, top: y, width: 1120 }, children: [_jsxs("button", { className: "flow-request-heading", style: { height: headingHeight }, "aria-label": `Step ${node.id}, jev, ${status}`, "aria-pressed": selected, onClick: () => onSelect(node.id), children: [_jsxs("span", { children: [_jsx("span", { className: "eyebrow", children: "Jev request" }), _jsx("strong", { children: node.title ?? node.id }), node.description && (_jsx("span", { className: "flow-request-description", children: node.description }))] }), _jsxs("span", { children: [status, trace.durationMs !== undefined
35
+ const ref = useNodeHeight(node.id, onResize);
36
+ return (_jsxs("article", { ref: ref, className: `flow-step flow-request ${selected ? "selected" : ""}`, "data-status": status, "aria-label": `Request ${node.id}`, style: { left: x, top: y, width: 1120 }, children: [_jsxs("button", { className: "flow-request-heading", "aria-label": `Step ${node.id}, jev, ${status}`, "aria-pressed": selected, onClick: () => onSelect(node.id), children: [_jsxs("span", { children: [_jsx("span", { className: "eyebrow", children: "Jev request" }), _jsx("strong", { children: node.title ?? node.id }), node.description && (_jsx("span", { className: "flow-request-description", children: node.description }))] }), _jsxs("span", { children: [status, trace.durationMs !== undefined
13
37
  ? ` · ${Math.round(trace.durationMs)} ms`
14
- : ""] })] }), _jsx(TraceDiagram, { trace: trace, embedded: true, onHeightChange: measure })] }));
38
+ : ""] })] }), _jsx(TraceDiagram, { trace: trace, embedded: true })] }));
15
39
  });
16
40
  function joinTitle(count) {
17
41
  return count === 2 ? "Wait for both checks" : `Wait for all ${count} paths`;
18
42
  }
19
- const Card = memo(function Card({ node, status, decision, selected, bounds, onSelect, lanes, inputs, }) {
43
+ const Card = memo(function Card({ node, status, decision, selected, bounds, onResize, onSelect, lanes, inputs, data, durationMs, collection, loop, collectionView, onCollectionChange, expansionId, }) {
44
+ const ref = useNodeHeight(node.id, onResize);
20
45
  const join = node.kind === "parallel";
46
+ const repeated = node.kind === "map" || node.kind === "loop";
47
+ const unit = node.kind === "loop" ? "Iteration" : "Item";
48
+ const completedItems = collection?.items.filter((item) => item.status === "completed").length ?? 0;
21
49
  const finished = lanes.filter((lane) => lane.status === "completed").length;
22
50
  const route = [...node.path]
23
51
  .reverse()
@@ -27,43 +55,77 @@ const Card = memo(function Card({ node, status, decision, selected, bounds, onSe
27
55
  : node.kind === "output" && status === "completed"
28
56
  ? "Returned outcome"
29
57
  : status;
30
- return (_jsxs("button", { className: `flow-step ${selected ? "selected" : ""}`, "data-status": status, style: {
31
- left: bounds.x,
32
- top: bounds.y,
33
- width: bounds.width,
34
- height: bounds.height,
35
- }, "aria-label": `Step ${node.id}, ${node.kind}, ${status}`, "aria-pressed": selected, onClick: () => onSelect(node.id), children: [_jsxs("span", { className: "flow-card-heading", children: [_jsx("span", { className: "flow-kind-icon", "aria-hidden": "true", children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: join ? (_jsx("path", { d: "M4 5h4l5 7h7M4 19h4l5-7m4-3 3 3-3 3" })) : node.kind === "output" ? (_jsxs(_Fragment, { children: [_jsx("rect", { x: "4", y: "4", width: "16", height: "16", rx: "4" }), _jsx("path", { d: "m8 12 3 3 5-6" })] })) : (_jsx("path", { d: "m12 3 9 9-9 9-9-9zM8 12h8" })) }) }), _jsx("span", { className: "eyebrow", children: join
36
- ? "Parallel checks"
37
- : node.kind === "branch"
38
- ? "Condition"
39
- : node.kind }), node.kind === "output" && (_jsx("span", { className: "flow-status", children: stateLabel }))] }), _jsx("strong", { children: node.title ?? (join ? joinTitle(lanes.length) : node.id) }), node.description && (_jsx("span", { className: "flow-step-description", children: node.description })), node.kind === "branch" &&
40
- (node.conditionRule !== undefined || node.condition) && (_jsxs("span", { className: "flow-condition-rule", children: [inputs && inputs.length > 0 && (_jsxs("span", { className: "flow-condition-incoming", children: [_jsx("span", { className: "flow-condition-label", children: inputs.length === 1 ? "Incoming value" : "Incoming values" }), inputs.slice(0, 3).map((input) => (_jsxs("span", { className: "flow-condition-input", children: [_jsx("span", { children: input.name }), _jsx("b", { children: input.value })] }, input.reference))), inputs.length > 3 && (_jsxs("small", { children: [inputs.length - 3, " more inputs \u00B7 select to inspect"] }))] })), _jsx("span", { className: "flow-condition-label", children: "Requirement" }), _jsx("code", { children: node.conditionRule !== undefined
41
- ? conditionRequirement(node.conditionRule, inputs ?? [])
42
- : node.condition })] })), join && (_jsxs(_Fragment, { children: [_jsxs("span", { className: "flow-join-summary", children: [joinTitle(lanes.length), " ", _jsxs("span", { children: [finished, "/", lanes.length] })] }), _jsxs("span", { className: "flow-lanes", children: [lanes.slice(0, 4).map((lane) => (_jsxs("span", { className: "flow-lane", "data-status": lane.status, children: [_jsxs("span", { children: [_jsx("span", { "aria-hidden": "true", children: lane.status === "completed"
43
- ? "✓"
44
- : lane.status === "error"
45
- ? "!"
46
- : "○" }), " ", lane.name] }), _jsx("small", { children: lane.status })] }, lane.name))), lanes.length > 4 && (_jsxs("small", { children: [lanes.length - 4, " more \u00B7 select to see all checks"] }))] })] })), _jsxs("span", { className: "flow-card-footer", children: [_jsx("code", { children: node.id }), _jsx("small", { children: join
47
- ? status === "completed"
48
- ? "All finished · continue"
49
- : stateLabel
50
- : node.kind === "output"
51
- ? route?.endsWith(":then")
52
- ? "Then route"
53
- : route?.endsWith(":else")
54
- ? "Else route"
55
- : "Final outcome"
58
+ const itemIndex = Math.min(collectionView?.index ?? 0, Math.max(0, (collection?.total ?? 0) - 1));
59
+ const expanded = collectionView?.open ?? false;
60
+ const valueCard = node.kind === "task" || node.kind === "merge";
61
+ const hasOutput = data !== null &&
62
+ typeof data === "object" &&
63
+ !Array.isArray(data) &&
64
+ Object.hasOwn(data, "output");
65
+ const footer = (_jsxs("span", { className: "flow-card-footer", children: [_jsx("code", { children: node.id }), _jsx("small", { children: join
66
+ ? status === "completed"
67
+ ? "All finished · continue"
68
+ : stateLabel
69
+ : node.kind === "output"
70
+ ? route?.endsWith(":then")
71
+ ? "Then route"
72
+ : route?.endsWith(":else")
73
+ ? "Else route"
74
+ : "Final outcome"
75
+ : node.kind === "merge" && decision !== undefined
76
+ ? `Selected ${decision ? "then" : "else"} value · continue`
77
+ : node.kind === "loop"
78
+ ? stateLabel
56
79
  : decision !== undefined
57
80
  ? decision
58
81
  ? "Condition met · then"
59
82
  : "Condition not met · else"
60
- : stateLabel })] }), node.kind === "jev" && (_jsx("small", { children: status === "untaken"
61
- ? "This request was not run."
62
- : "Request details are not captured yet or no longer retained." }))] }));
83
+ : stateLabel })] }));
84
+ return (_jsxs("article", { ref: ref, className: `flow-step ${node.kind === "task" ? "flow-task" : ""} ${selected ? "selected" : ""}`, "data-status": status, style: {
85
+ left: bounds.x,
86
+ top: bounds.y,
87
+ width: bounds.width,
88
+ }, "aria-label": `${node.kind === "map" ? "Collection" : "Step"} ${node.title ?? node.id}`, children: [_jsxs("button", { className: "flow-card-body", "aria-label": `Step ${node.id}, ${node.kind}, ${status}`, "aria-pressed": selected, onClick: () => onSelect(node.id), children: [_jsxs("span", { className: "flow-card-heading", children: [_jsx("span", { className: "flow-kind-icon", "aria-hidden": "true", children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: join || node.kind === "merge" ? (_jsx("path", { d: "M4 5h4l5 7h7M4 19h4l5-7m4-3 3 3-3 3" })) : node.kind === "loop" ? (_jsx("path", { d: "M20 7a8 8 0 1 0 0 10M20 3v5h-5" })) : node.kind === "map" ? (_jsxs(_Fragment, { children: [_jsx("rect", { x: "8", y: "3", width: "12", height: "5", rx: "1" }), _jsx("rect", { x: "8", y: "10", width: "12", height: "5", rx: "1" }), _jsx("rect", { x: "8", y: "17", width: "12", height: "5", rx: "1" }), _jsx("path", { d: "M2 5.5h3m-3 7h3m-3 7h3" })] })) : node.kind === "task" ? (_jsx("path", { d: "m8 6-6 6 6 6m8-12 6 6-6 6m-3-15-2 18" })) : node.kind === "output" ? (_jsxs(_Fragment, { children: [_jsx("rect", { x: "4", y: "4", width: "16", height: "16", rx: "4" }), _jsx("path", { d: "m8 12 3 3 5-6" })] })) : (_jsx("path", { d: "m12 3 9 9-9 9-9-9zM8 12h8" })) }) }), _jsx("span", { className: "eyebrow", children: join
89
+ ? "Parallel checks"
90
+ : node.kind === "branch" || node.kind === "if"
91
+ ? "Condition"
92
+ : node.kind === "task"
93
+ ? "Function"
94
+ : node.kind === "map"
95
+ ? "Collection"
96
+ : node.kind }), node.kind === "output" && (_jsx("span", { className: "flow-status", children: stateLabel }))] }), _jsx("strong", { children: node.title ?? (join ? joinTitle(lanes.length) : node.id) }), node.description && (_jsx("span", { className: "flow-step-description", children: node.description })), repeated && (_jsxs("span", { className: "flow-map-progress", children: [_jsx("strong", { children: node.kind === "loop"
97
+ ? `${completedItems} iteration${completedItems === 1 ? "" : "s"} completed`
98
+ : collection
99
+ ? `${completedItems} of ${collection.total} complete`
100
+ : "Waiting for collection" }), loop ? (_jsx("span", { children: loop.stopReason === "limit"
101
+ ? "Iteration limit reached · incomplete"
102
+ : loop.stopReason === "condition"
103
+ ? "Condition is false · finished"
104
+ : `Up to ${loop.maxIterations} iteration${loop.maxIterations === 1 ? "" : "s"}` })) : (collection && (_jsxs("span", { children: [collection.items.filter((item) => item.status === "running")
105
+ .length, " ", "running \u00B7", " ", collection.items.filter((item) => item.status === "waiting")
106
+ .length, " ", "queued \u00B7 up to ", collection.concurrency, " concurrent"] }))), node.kind === "loop" && node.conditionRule && (_jsxs("code", { children: ["Continue while ", formatFlowCondition(node.conditionRule)] })), _jsxs("small", { children: ["Explore ", node.kind === "loop" ? "an iteration's" : "an item's", " ", "connected flow below"] })] })), (node.kind === "branch" || node.kind === "if") &&
107
+ (node.conditionRule !== undefined || node.condition) && (_jsxs("span", { className: "flow-condition-rule", children: [inputs && inputs.length > 0 && (_jsxs("span", { className: "flow-condition-incoming", children: [_jsx("span", { className: "flow-condition-label", children: inputs.length === 1 ? "Incoming value" : "Incoming values" }), inputs.slice(0, 3).map((input) => (_jsxs("span", { className: "flow-condition-input", children: [_jsx("span", { children: input.name }), _jsx("b", { children: input.value })] }, input.reference))), inputs.length > 3 && (_jsxs("small", { children: [inputs.length - 3, " more inputs \u00B7 select to inspect"] }))] })), _jsx("span", { className: "flow-condition-label", children: "Requirement" }), _jsx("code", { children: node.conditionRule !== undefined
108
+ ? conditionRequirement(node.conditionRule, inputs ?? [])
109
+ : node.condition })] })), join && (_jsxs(_Fragment, { children: [_jsxs("span", { className: "flow-join-summary", children: [joinTitle(lanes.length), " ", _jsxs("span", { children: [finished, "/", lanes.length] })] }), _jsxs("span", { className: "flow-lanes", children: [lanes.slice(0, 4).map((lane) => (_jsxs("span", { className: "flow-lane", "data-status": lane.status, children: [_jsxs("span", { children: [_jsx("span", { "aria-hidden": "true", children: lane.status === "completed"
110
+ ? "✓"
111
+ : lane.status === "error"
112
+ ? "!"
113
+ : "○" }), " ", lane.name] }), _jsx("small", { children: lane.status })] }, lane.name))), lanes.length > 4 && (_jsxs("small", { children: [lanes.length - 4, " more \u00B7 select to see all checks"] }))] })] })), !valueCard && footer, node.kind === "jev" && (_jsx("small", { children: status === "untaken"
114
+ ? "This request was not run."
115
+ : "Request details are not captured yet or no longer retained." }))] }), valueCard && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flow-task-preview", children: [_jsx("span", { className: "flow-condition-label", children: "Returned value" }), hasOutput ? (_jsx(DiagramContent, { value: data.output, label: `Returned value for ${node.title ?? node.id}` })) : (_jsx("span", { className: "diagram-value-empty", children: status === "running"
116
+ ? "Running application code…"
117
+ : "No output captured" })), _jsx("small", { children: durationMs === undefined
118
+ ? "Select the heading to inspect inputs and output"
119
+ : `${Math.round(durationMs)} ms · select heading to inspect` })] }), footer] })), repeated && (_jsxs("div", { className: "flow-collection-controls", "data-diagram-scroll": true, children: [_jsxs("label", { children: [node.kind === "loop" ? "Iteration" : "Collection item", _jsxs("select", { "aria-label": `${node.kind === "loop" ? "Iteration" : "Collection item"} for ${node.title ?? node.id}`, value: itemIndex, disabled: !collection?.total, onChange: (event) => onCollectionChange(node.id, {
120
+ index: Number(event.target.value),
121
+ open: expanded,
122
+ }), children: [!collection?.total && (_jsx("option", { value: 0, children: node.kind === "loop" ? "No iterations" : "No items" })), collection?.items.map((item) => (_jsxs("option", { value: item.index, children: [unit, " ", item.index + 1, " \u00B7 ", item.status] }, item.index)))] })] }), _jsx("button", { disabled: !collection?.total || !node.itemDefinition, "aria-expanded": expanded, "aria-controls": expanded ? expansionId : undefined, onClick: () => onCollectionChange(node.id, { index: itemIndex, open: !expanded }), children: expanded ? "Hide Flow" : "View Flow" })] }))] }));
63
123
  });
64
- export const FlowOverview = memo(function FlowOverview({ definition, run, traces, }) {
124
+ export const FlowOverview = memo(function FlowOverview({ definition, run, traces, inline = false, }) {
65
125
  const [selected, setSelected] = useState(() => definition.nodes.find((n) => n.kind === "jev")?.id ?? "");
66
126
  const [details, setDetails] = useState(false);
127
+ const [collectionViews, setCollectionViews] = useState({});
128
+ const [itemSizes, setItemSizes] = useState({});
67
129
  const [heights, setHeights] = useState({});
68
130
  const tracesById = useMemo(() => new Map(traces.map((trace) => [trace.id, trace])), [traces]);
69
131
  const inputsByStep = useMemo(() => new Map(definition.nodes
@@ -90,7 +152,8 @@ export const FlowOverview = memo(function FlowOverview({ definition, run, traces
90
152
  const parents = new Map(definition.nodes.map((node) => [node.id, ancestors(node.id)]));
91
153
  return new Map(definition.nodes.map((node) => [
92
154
  node.id,
93
- node.dependencies.filter((id) => !node.dependencies.some((other) => other !== id && parents.get(other)?.has(id))),
155
+ node.dependencies.filter((id) => node.kind === "merge" ||
156
+ !node.dependencies.some((other) => other !== id && parents.get(other)?.has(id))),
94
157
  ]));
95
158
  }, [definition]);
96
159
  const parallelLanes = useMemo(() => new Map(definition.nodes
@@ -135,6 +198,7 @@ export const FlowOverview = memo(function FlowOverview({ definition, run, traces
135
198
  remaining = remaining.filter((node) => !ids.has(node.id));
136
199
  }
137
200
  const points = new Map();
201
+ const expansions = new Map();
138
202
  let x = 40;
139
203
  for (let column = 0; column <= Math.max(0, ...columns.values()); column++) {
140
204
  const nodes = definition.nodes.filter((node) => columns.get(node.id) === column);
@@ -142,17 +206,20 @@ export const FlowOverview = memo(function FlowOverview({ definition, run, traces
142
206
  for (const node of nodes) {
143
207
  const captured = node.kind === "jev" &&
144
208
  tracesById.has(run.steps[node.id]?.traceId ?? "");
145
- const w = captured ? 1120 : 360, h = captured
146
- ? (heights[node.id] ?? 640)
147
- : node.kind === "parallel"
148
- ? 190 +
149
- (node.description ? 64 : 0) +
150
- Math.min(4, (node.lanes ?? node.dependencies).length) * 32
151
- : (node.description ? 244 : 172) +
152
- (node.kind === "branch"
153
- ? 110 +
154
- Math.min(3, inputsByStep.get(node.id)?.length ?? 0) * 24
155
- : 0);
209
+ const w = captured ? 1120 : 360, h = heights[node.id] ??
210
+ (captured
211
+ ? 640
212
+ : node.kind === "parallel"
213
+ ? 190 +
214
+ (node.description ? 64 : 0) +
215
+ Math.min(4, (node.lanes ?? node.dependencies).length) * 32
216
+ : (node.description ? 244 : 172) +
217
+ (node.kind === "branch" || node.kind === "if"
218
+ ? 110 +
219
+ Math.min(3, inputsByStep.get(node.id)?.length ?? 0) * 24
220
+ : node.kind === "task" || node.kind === "merge"
221
+ ? 130
222
+ : 0));
156
223
  const parents = (links.get(node.id) ?? [])
157
224
  .map((id) => points.get(id))
158
225
  .filter(Boolean);
@@ -167,31 +234,84 @@ export const FlowOverview = memo(function FlowOverview({ definition, run, traces
167
234
  }
168
235
  x += width + 160;
169
236
  }
237
+ // Collection previews occupy a separate row below the original graph.
238
+ // Their dimensions must never participate in its column or lane spacing.
239
+ const expansionY = Math.max(...[...points.values()].map((p) => p.y + p.height), 40) + 96;
240
+ for (const node of definition.nodes) {
241
+ if (!collectionViews[node.id]?.open ||
242
+ !node.itemDefinition ||
243
+ !run.steps[node.id]?.collection?.total)
244
+ continue;
245
+ const card = points.get(node.id);
246
+ if (!card)
247
+ continue;
248
+ const size = itemSizes[node.id] ?? { width: 2400, height: 800 };
249
+ const left = card.x + card.width / 2 - size.width / 2;
250
+ let top = expansionY;
251
+ // Center each preview on its collection. Stack overlapping previews
252
+ // vertically, leaving the original graph and all its coordinates alone.
253
+ for (const area of expansions.values()) {
254
+ if (left - 80 < area.x + area.width + 80 &&
255
+ left + size.width + 80 > area.x - 80)
256
+ top = Math.max(top, area.y + area.height + 96);
257
+ }
258
+ expansions.set(node.id, { x: left, y: top, ...size });
259
+ }
260
+ // External arrows and centered previews can extend left of canvas zero.
261
+ const left = Math.min(0, ...[...expansions.values()].map((p) => p.x - 80));
262
+ const right = Math.max(700, x - 120, ...[...expansions.values()].map((p) => p.x + p.width + 80));
170
263
  return {
171
264
  points,
172
- width: Math.max(700, x - 120),
173
- height: Math.max(400, ...[...points.values()].map((p) => p.y + p.height + 40)),
265
+ expansions,
266
+ left,
267
+ width: right - left,
268
+ height: Math.max(400, ...[...points.values(), ...expansions.values()].map((p) => p.y + p.height + 40)),
174
269
  };
175
- }, [definition, run.steps, tracesById, heights, links, inputsByStep]);
176
- const { viewport, canvas, zoom, zoomBy, fit, preserveCamera, focusArea } = useDiagramViewport(layout.width, layout.height, run.id);
270
+ }, [
271
+ definition,
272
+ run.steps,
273
+ tracesById,
274
+ heights,
275
+ links,
276
+ inputsByStep,
277
+ collectionViews,
278
+ itemSizes,
279
+ ]);
280
+ const { viewport, canvas, zoom, zoomBy, fit, preserveCamera, focusArea } = useDiagramViewport(layout.width, layout.height, run.id, {
281
+ x: layout.left,
282
+ y: 0,
283
+ });
177
284
  const onResize = useCallback((id, height) => {
178
285
  preserveCamera();
179
286
  setHeights((previous) => previous[id] === height ? previous : { ...previous, [id]: height });
180
287
  }, [preserveCamera]);
181
288
  const started = useRef(false);
289
+ const changeCollection = useCallback((id, view) => {
290
+ preserveCamera();
291
+ setSelected(id);
292
+ started.current = true;
293
+ setCollectionViews((previous) => ({ ...previous, [id]: view }));
294
+ }, [preserveCamera]);
295
+ const resizeItem = useCallback((id, size) => {
296
+ preserveCamera();
297
+ setItemSizes((previous) => previous[id]?.width === size.width &&
298
+ previous[id]?.height === size.height
299
+ ? previous
300
+ : { ...previous, [id]: size });
301
+ }, [preserveCamera]);
182
302
  useEffect(() => {
183
- if (started.current)
303
+ if (inline || started.current)
184
304
  return;
185
305
  const first = layout.points.get(selected);
186
306
  if (first) {
187
307
  started.current = true;
188
308
  focusArea(first);
189
309
  }
190
- }, [layout, selected, focusArea]);
310
+ }, [layout, selected, focusArea, inline]);
191
311
  const selectStep = (id) => {
192
312
  setSelected(id);
193
313
  const bounds = layout.points.get(id);
194
- if (bounds)
314
+ if (bounds && !inline)
195
315
  focusArea(bounds);
196
316
  };
197
317
  const node = definition.nodes.find((n) => n.id === selected);
@@ -199,30 +319,122 @@ export const FlowOverview = memo(function FlowOverview({ definition, run, traces
199
319
  const trace = step?.traceId
200
320
  ? traces.find((t) => t.id === step.traceId)
201
321
  : undefined;
202
- return (_jsxs("section", { className: "request-detail whole-flow", "aria-label": "Whole flow", children: [_jsxs("header", { className: "detail-header", children: [_jsxs("div", { children: [_jsxs("span", { className: "eyebrow", children: ["Flow run \u00B7 ", run.status] }), _jsx("h2", { children: run.name })] }), _jsxs("div", { children: [run.outcome ? (_jsxs("strong", { children: ["Outcome: ", run.outcome] })) : ((run.error ?? "Execution paths")), _jsx("small", { className: "muted", children: run.durationMs !== undefined
203
- ? ` · ${Math.round(run.durationMs)} ms`
204
- : "" })] })] }), _jsxs("div", { className: "lab-flow-toolbar", children: [_jsxs("div", { children: [_jsx("button", { "aria-label": "Zoom flow out", onClick: () => zoomBy(1 / 1.2), children: "\u2212" }), _jsxs("output", { "aria-label": "Flow zoom", children: [zoom, "%"] }), _jsx("button", { "aria-label": "Zoom flow in", onClick: () => zoomBy(1.2), children: "+" }), _jsx("button", { onClick: fit, children: "Fit flow" }), _jsx("button", { disabled: !selected, onClick: () => selectStep(selected), children: "Focus step" })] }), _jsxs("label", { children: ["Step", _jsxs("select", { "aria-label": "Flow step", value: selected, onChange: (e) => selectStep(e.target.value), children: [_jsx("option", { value: "", children: "Select a step" }), definition.nodes.map((n) => (_jsxs("option", { value: n.id, children: [n.title ? `${n.title} (${n.id})` : n.id, " \u00B7", " ", run.steps[n.id]?.status ?? "waiting"] }, n.id)))] })] })] }), _jsx("div", { className: "flow-overview-viewport diagram-viewport", ref: viewport, tabIndex: 0, "aria-label": "Flow canvas", role: "region", children: _jsxs("div", { ref: canvas, className: "flow-overview-canvas", style: { width: layout.width, height: layout.height }, children: [_jsxs("svg", { width: layout.width, height: layout.height, "aria-hidden": "true", className: "flow-connections", children: [_jsx("defs", { children: _jsx("marker", { id: `arrow-${run.id}`, viewBox: "0 0 10 10", refX: "8", refY: "5", markerWidth: "5", markerHeight: "5", orient: "auto", children: _jsx("path", { d: "M0 0L10 5L0 10z", fill: "currentColor" }) }) }), definition.nodes.flatMap((n) => (links.get(n.id) ?? []).map((id) => {
205
- const a = layout.points.get(id), b = layout.points.get(n.id);
206
- if (!a || !b)
207
- return null;
208
- const active = ["running", "completed"].includes(run.steps[n.id]?.status);
209
- return (_jsx("path", { d: `M${a.x + a.width},${a.y + a.height / 2} C${a.x + a.width + 80},${a.y + a.height / 2} ${b.x - 80},${b.y + b.height / 2} ${b.x},${b.y + b.height / 2}`, stroke: active ? "#E551BA" : "currentColor", opacity: active ? 1 : 0.35, fill: "none", strokeWidth: "2", markerEnd: `url(#arrow-${run.id})` }, `${id}-${n.id}`));
210
- }))] }), definition.nodes.map((n) => {
211
- const p = layout.points.get(n.id);
212
- if (!p)
213
- return null;
214
- const captured = tracesById.get(run.steps[n.id]?.traceId ?? "");
215
- return n.kind === "jev" && captured ? (_jsx(RequestNode, { node: n, trace: captured, status: run.steps[n.id]?.status ?? "waiting", selected: selected === n.id, x: p.x, y: p.y, onSelect: setSelected, onResize: onResize }, n.id)) : (_jsx(Card, { node: n, status: run.steps[n.id]?.status ?? "waiting", decision: run.steps[n.id]?.decision, selected: selected === n.id, bounds: p, onSelect: setSelected, inputs: inputsByStep.get(n.id), lanes: parallelLanes.get(n.id) ?? [] }, n.id));
216
- })] }) }), _jsxs("div", { className: "flow-selection", children: [node && step ? (_jsxs(_Fragment, { children: [_jsxs("strong", { children: [node.title ?? node.id, " \u00B7 ", step.status] }), _jsx("span", { className: "muted", children: step.durationMs !== undefined
217
- ? ` · ${Math.round(step.durationMs)} ms`
218
- : "" }), node.description && _jsx("p", { children: node.description }), node.kind === "parallel" && (_jsxs("p", { children: ["These paths run at the same time:", " ", (node.lanes ?? node.dependencies).join(", "), ". The next step starts only after all finish successfully. This marker waits for their results; it does not make another Jev request."] })), node.kind === "parallel" && (_jsx("ul", { className: "flow-lane-details", children: parallelLanes.get(node.id)?.map((lane) => (_jsxs("li", { children: [lane.name, " \u00B7 ", lane.status] }, lane.name))) })), (inputsByStep.get(node.id)?.length ?? 0) > 0 && (_jsx("dl", { className: "flow-input-details", children: inputsByStep.get(node.id)?.map((input) => (_jsxs("div", { children: [_jsxs("dt", { children: [input.name, " ", _jsx("code", { children: input.reference })] }), _jsx("dd", { children: input.value })] }, input.reference))) })), (node.conditionRule !== undefined || node.condition) && (_jsxs("p", { children: [_jsx("code", { children: node.conditionRule !== undefined
219
- ? formatFlowCondition(node.conditionRule)
220
- : node.condition }), " ", "\u00B7", " ", step.decision === undefined
221
- ? "Not evaluated"
222
- : step.decision
223
- ? "Passed · then route"
224
- : "Not met · else route"] })), _jsx("p", { className: "muted", children: node.path.length ? node.path.join(" / ") : "Main path" }), step.error && _jsx("p", { role: "alert", children: step.error }), node.kind === "jev" &&
225
- (trace ? (_jsx("button", { onClick: () => setDetails(!details), children: details ? "Hide request details" : "Inspect request" })) : (_jsx("p", { children: step.traceId
226
- ? "Request capture was cleared or evicted."
227
- : "No captured request linked yet. Use a client created by this inspector." })))] })) : (_jsx("p", { className: "muted", children: "Each request diagram shows its context, question and answer. Use its question picker for other answers. Lines between requests show execution dependencies." })), run.value !== undefined && (_jsxs("details", { children: [_jsx("summary", { children: "Captured outcome value" }), _jsx(JsonBlock, { value: run.value })] }))] }), details && trace && (_jsx("div", { className: "flow-request-detail", children: _jsx(RequestDetail, { trace: trace }) }))] }));
322
+ return (_jsx("div", { className: `flow-overview-stack ${inline ? "flow-inline-overview" : ""}`, style: inline ? { width: layout.width } : undefined, children: _jsxs("section", { className: "request-detail whole-flow", "aria-label": inline ? "Collection item flow" : "Whole flow", children: [!inline && (_jsxs("header", { className: "detail-header", children: [_jsxs("div", { children: [_jsxs("span", { className: "eyebrow", children: ["Flow run \u00B7 ", run.status] }), _jsx("h2", { children: run.name })] }), _jsxs("div", { children: [run.outcome ? (_jsxs("strong", { children: ["Outcome: ", run.outcome] })) : (run.error ?? "Execution paths"), _jsx("small", { className: "muted", children: run.durationMs !== undefined
323
+ ? ` · ${Math.round(run.durationMs)} ms`
324
+ : "" })] })] })), _jsxs("div", { className: "lab-flow-toolbar", children: [!inline && (_jsxs("div", { children: [_jsx("button", { "aria-label": "Zoom flow out", onClick: () => zoomBy(1 / 1.2), children: "\u2212" }), _jsxs("output", { "aria-label": "Flow zoom", children: [zoom, "%"] }), _jsx("button", { "aria-label": "Zoom flow in", onClick: () => zoomBy(1.2), children: "+" }), _jsx("button", { onClick: fit, children: "Fit flow" }), _jsx("button", { disabled: !selected, onClick: () => selectStep(selected), children: "Focus step" })] })), _jsxs("label", { children: ["Step", _jsxs("select", { "aria-label": "Flow step", value: selected, onChange: (e) => selectStep(e.target.value), children: [_jsx("option", { value: "", children: "Select a step" }), definition.nodes.map((n) => (_jsxs("option", { value: n.id, children: [n.title ? `${n.title} (${n.id})` : n.id, " \u00B7", " ", run.steps[n.id]?.status ?? "waiting"] }, n.id)))] })] })] }), _jsxs("div", { className: "flow-workspace", children: [_jsx("div", { className: inline
325
+ ? "flow-inline-surface"
326
+ : "flow-overview-viewport diagram-viewport", ref: inline ? undefined : viewport, style: inline ? { height: layout.height } : undefined, tabIndex: inline ? undefined : 0, "aria-label": inline ? "Item flow canvas" : "Flow canvas", role: "region", children: _jsxs("div", { ref: inline ? undefined : canvas, className: "flow-overview-canvas", style: {
327
+ width: layout.width,
328
+ height: layout.height,
329
+ // Inline views have no camera to account for negative bounds.
330
+ // Keep nested previews and their external arrows in the frame.
331
+ transform: inline ? `translateX(${-layout.left}px)` : undefined,
332
+ }, children: [_jsxs("svg", { width: layout.width, height: layout.height, "aria-hidden": "true", className: "flow-connections", children: [_jsx("defs", { children: _jsx("marker", { id: `arrow-${run.id}`, viewBox: "0 0 10 10", refX: "8", refY: "5", markerWidth: "5", markerHeight: "5", orient: "auto", children: _jsx("path", { d: "M0 0L10 5L0 10z", fill: "currentColor" }) }) }), definition.nodes.flatMap((n) => (links.get(n.id) ?? []).map((id) => {
333
+ const a = layout.points.get(id), b = layout.points.get(n.id);
334
+ if (!a || !b)
335
+ return null;
336
+ const decision = n.merge
337
+ ? run.steps[n.merge.condition]?.decision
338
+ : undefined;
339
+ const active = ["running", "completed"].includes(run.steps[n.id]?.status) &&
340
+ ["running", "completed"].includes(run.steps[id]?.status) &&
341
+ (!n.merge ||
342
+ (decision !== undefined &&
343
+ id === n.merge[decision ? "then" : "else"]));
344
+ return (_jsx("path", { d: `M${a.x + a.width},${a.y + a.height / 2} C${a.x + a.width + 80},${a.y + a.height / 2} ${b.x - 80},${b.y + b.height / 2} ${b.x},${b.y + b.height / 2}`, stroke: active ? "#E551BA" : "currentColor", opacity: active ? 1 : 0.35, fill: "none", strokeWidth: "2", markerEnd: `url(#arrow-${run.id})` }, `${id}-${n.id}`));
345
+ })), [...layout.expansions].map(([id, area]) => {
346
+ const card = layout.points.get(id);
347
+ const x = card.x + card.width / 2;
348
+ const endX = area.x + area.width / 2;
349
+ const startY = card.y + card.height;
350
+ const bendY = startY + (area.y - startY) / 2;
351
+ return (_jsxs("g", { className: "flow-collection-link", children: [_jsx("path", { d: `M${x},${startY} C${x},${bendY} ${endX},${bendY} ${endX},${area.y}`, fill: "none", stroke: "#E551BA", strokeWidth: "2", strokeDasharray: "6 5", markerEnd: `url(#arrow-${run.id})` }), _jsxs("text", { x: endX + 14, y: area.y - 20, children: [definition.nodes.find((n) => n.id === id)?.kind ===
352
+ "loop"
353
+ ? "Selected iteration"
354
+ : "Selected item", " ", (collectionViews[id]?.index ?? 0) + 1] })] }, id));
355
+ })] }), definition.nodes.map((n) => {
356
+ const p = layout.points.get(n.id);
357
+ if (!p)
358
+ return null;
359
+ const captured = tracesById.get(run.steps[n.id]?.traceId ?? "");
360
+ return n.kind === "jev" && captured ? (_jsx(RequestNode, { node: n, trace: captured, status: run.steps[n.id]?.status ?? "waiting", selected: selected === n.id, x: p.x, y: p.y, onSelect: setSelected, onResize: onResize }, n.id)) : (_jsx(Card, { node: n, status: run.steps[n.id]?.status ?? "waiting", decision: run.steps[n.id]?.decision, selected: selected === n.id, bounds: p, onResize: onResize, onSelect: setSelected, inputs: inputsByStep.get(n.id), data: run.steps[n.id]?.data, durationMs: run.steps[n.id]?.durationMs, lanes: parallelLanes.get(n.id) ?? [], collection: run.steps[n.id]?.collection, loop: run.steps[n.id]?.loop, collectionView: collectionViews[n.id], onCollectionChange: changeCollection, expansionId: `collection-${run.id}-${n.id}` }, n.id));
361
+ }), [...layout.expansions].map(([id, bounds]) => {
362
+ const collectionNode = definition.nodes.find((n) => n.id === id);
363
+ const collection = run.steps[id].collection;
364
+ const index = Math.min(collectionViews[id].index, collection.total - 1);
365
+ return (_jsx(InlineItemFlow, { id: `collection-${run.id}-${id}`, node: collectionNode, collection: collection, index: index, bounds: bounds, traces: traces, onResize: resizeItem, onCollectionChange: changeCollection }, id));
366
+ })] }) }), _jsxs(FlowDetailsPanel, { inline: inline, selection: node?.title ?? node?.id, onLayoutChange: preserveCamera, children: [_jsxs("div", { className: "flow-selection", children: [node && step ? (_jsxs(_Fragment, { children: [_jsxs("strong", { children: [node.title ?? node.id, " \u00B7 ", step.status] }), _jsx("span", { className: "muted", children: step.durationMs !== undefined
367
+ ? ` · ${Math.round(step.durationMs)} ms`
368
+ : "" }), node.description && _jsx("p", { children: node.description }), node.kind === "loop" && (_jsxs("div", { className: "flow-task-details", children: [_jsx("p", { children: step.loop?.stopReason === "limit"
369
+ ? "Iteration limit reached. The final state is incomplete and downstream steps did not run."
370
+ : step.loop?.stopReason === "condition"
371
+ ? "The continuation condition became false. The loop returned its final state."
372
+ : "Iterations execute sequentially, carrying state forward." }), _jsx(DiagramContent, { value: step.data ?? null, label: "Loop initial and latest state" })] })), (node.kind === "task" || node.kind === "merge") && (_jsxs("div", { className: "flow-task-details", children: [_jsxs("p", { className: "muted", children: [node.kind === "merge"
373
+ ? "Value returned by the selected path. This merge does not make another request."
374
+ : "Application function · runs on your server.", " ", "Captured values may be redacted or truncated."] }), step.data &&
375
+ typeof step.data === "object" &&
376
+ !Array.isArray(step.data) ? (["input", "output"]
377
+ .filter((key) => Object.hasOwn(step.data, key))
378
+ .map((key) => (_jsxs("details", { open: true, children: [_jsx("summary", { children: key === "input"
379
+ ? "Function inputs"
380
+ : node.kind === "merge"
381
+ ? "Selected value"
382
+ : "Function output" }), _jsx(DiagramContent, { value: step.data[key], label: node.kind === "merge"
383
+ ? "Selected value"
384
+ : `Function ${key}` })] }, key)))) : (_jsx("p", { children: typeof step.data === "string"
385
+ ? step.data
386
+ : node.kind === "merge"
387
+ ? "No selected value captured yet."
388
+ : "No function data captured yet." })), step.captureWarnings?.map((warning) => (_jsx("p", { className: "muted", children: warning }, warning)))] })), node.kind === "parallel" && (_jsxs("p", { children: ["These paths run at the same time:", " ", (node.lanes ?? node.dependencies).join(", "), ". The next step starts only after all finish successfully. This marker waits for their results; it does not make another Jev request."] })), node.kind === "parallel" && (_jsx("ul", { className: "flow-lane-details", children: parallelLanes.get(node.id)?.map((lane) => (_jsxs("li", { children: [lane.name, " \u00B7 ", lane.status] }, lane.name))) })), (inputsByStep.get(node.id)?.length ?? 0) > 0 && (_jsx("dl", { className: "flow-input-details", children: inputsByStep.get(node.id)?.map((input) => (_jsxs("div", { children: [_jsxs("dt", { children: [input.name, " ", _jsx("code", { children: input.reference })] }), _jsx("dd", { children: input.value })] }, input.reference))) })), (node.conditionRule !== undefined || node.condition) && (_jsxs("p", { children: [_jsx("code", { children: node.conditionRule !== undefined
389
+ ? formatFlowCondition(node.conditionRule)
390
+ : node.condition }), " ", "\u00B7", " ", step.decision === undefined
391
+ ? "Not evaluated"
392
+ : step.decision
393
+ ? node.kind === "loop"
394
+ ? "Continue"
395
+ : "Passed · then route"
396
+ : node.kind === "loop"
397
+ ? "Stop"
398
+ : "Not met · else route"] })), _jsx("p", { className: "muted", children: node.path.length ? node.path.join(" / ") : "Main path" }), step.error && _jsx("p", { role: "alert", children: step.error }), node.kind === "jev" &&
399
+ (trace ? (_jsx("button", { onClick: () => setDetails(!details), children: details ? "Hide request details" : "Inspect request" })) : (_jsx("p", { children: step.traceId
400
+ ? "Request capture was cleared or evicted."
401
+ : "No captured request linked yet. Use a client created by this inspector." })))] })) : (_jsx("p", { className: "muted", children: "Each request diagram shows its context, question and answer. Use its question picker for other answers. Lines between requests show execution dependencies." })), run.value !== undefined && (_jsxs("details", { children: [_jsx("summary", { children: "Captured outcome value" }), _jsx(JsonBlock, { value: run.value })] }))] }), details && trace && (_jsx("div", { className: "flow-request-detail", children: _jsx(RequestDetail, { trace: trace }) }))] })] })] }) }));
402
+ });
403
+ const InlineItemFlow = memo(function InlineItemFlow({ id, node, collection, index, bounds, traces, onResize, onCollectionChange, }) {
404
+ const ref = useRef(null);
405
+ const item = collection.items[index];
406
+ const unit = node.kind === "loop" ? "iteration" : "collection item";
407
+ const [direction, setDirection] = useState(1);
408
+ const cycleItem = (direction) => {
409
+ if (collection.total <= 1)
410
+ return;
411
+ setDirection(direction);
412
+ onCollectionChange(node.id, {
413
+ index: (index + direction + collection.total) % collection.total,
414
+ open: true,
415
+ });
416
+ };
417
+ useEffect(() => {
418
+ const element = ref.current;
419
+ if (!element)
420
+ return;
421
+ let width = 0, height = 0;
422
+ const measure = () => {
423
+ const nextWidth = element.offsetWidth, nextHeight = element.offsetHeight;
424
+ if (nextWidth > 0 &&
425
+ nextHeight > 0 &&
426
+ (width !== nextWidth || height !== nextHeight)) {
427
+ width = nextWidth;
428
+ height = nextHeight;
429
+ onResize(node.id, { width, height });
430
+ }
431
+ };
432
+ measure();
433
+ const observer = new ResizeObserver(measure);
434
+ observer.observe(element);
435
+ return () => observer.disconnect();
436
+ }, [node.id, onResize]);
437
+ return (_jsxs("article", { ref: ref, id: id, className: "flow-collection-expansion", "aria-label": `Selected ${unit} flow for ${node.title ?? node.id}`, style: { left: bounds.x, top: bounds.y }, children: [_jsx("button", { className: "flow-item-arrow flow-item-previous", "aria-label": `Previous ${unit}`, "aria-controls": `${id}-item`, title: `Previous ${unit}`, disabled: collection.total <= 1, onClick: () => cycleItem(-1), children: _jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m14 6-6 6 6 6" }) }) }), _jsxs("div", { className: "flow-item-frame", children: [_jsxs("header", { className: "flow-collection-heading", children: [_jsxs("div", { children: [_jsxs("span", { className: "eyebrow", children: ["Selected ", unit] }), _jsxs("h3", { "aria-live": "polite", "aria-atomic": "true", children: [node.title ?? node.id, " \u00B7", " ", node.kind === "loop" ? "Iteration" : "Item", " ", index + 1, " of", " ", collection.total] }), _jsx("small", { children: item?.status ?? "waiting" })] }), _jsx("button", { onClick: () => onCollectionChange(node.id, { index, open: false }), children: "Hide Flow" })] }), _jsx("div", { id: `${id}-item`, className: `flow-item-content ${direction < 0 ? "flow-item-backward" : "flow-item-forward"}`, children: item?.run && node.itemDefinition ? (_jsx(FlowOverview, { definition: node.itemDefinition, run: item.run, traces: traces, inline: true }, item.run.id)) : (_jsx("p", { className: "flow-item-placeholder", children: item?.status === "waiting"
438
+ ? "This item is queued. Its flow will appear here when it starts."
439
+ : "This item has no captured execution. It may have been cancelled or blocked before starting." })) }, index)] }), _jsx("button", { className: "flow-item-arrow flow-item-next", "aria-label": `Next ${unit}`, "aria-controls": `${id}-item`, title: `Next ${unit}`, disabled: collection.total <= 1, onClick: () => cycleItem(1), children: _jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m10 6 6 6-6 6" }) }) })] }));
228
440
  });
@@ -50,10 +50,8 @@ export function JevDrawer({ children, open, onOpenChange, }) {
50
50
  }, [open, root]);
51
51
  useEffect(() => {
52
52
  const host = document.createElement("jev-devtools-root");
53
- // The host is the only element exposed to page selectors. The overlay itself
54
- // lives in the shadow tree, with its own typography and component styles.
55
- host.style.cssText =
56
- "all: initial !important; position: fixed !important; inset: 0 !important; pointer-events: none !important; z-index: 2147483000 !important;";
53
+ // The shadow stylesheet resets the exposed host through :host(), keeping
54
+ // page styles out without an expanded inline reset on the host element.
57
55
  const shadow = host.attachShadow({ mode: "open" });
58
56
  const style = document.createElement("style");
59
57
  style.textContent = inspectorStyles;
@@ -17,6 +17,15 @@ export function conditionInputs(rule, run, traces, stepNames = new Map()) {
17
17
  : undefined;
18
18
  let value = trace?.result;
19
19
  let available = !!trace?.result;
20
+ if (node.source === "task" && typeof node.stepId === "string") {
21
+ const data = run.steps[node.stepId]?.data;
22
+ available =
23
+ !!data &&
24
+ typeof data === "object" &&
25
+ !Array.isArray(data) &&
26
+ Object.hasOwn(data, "output");
27
+ value = available ? data.output : undefined;
28
+ }
20
29
  if (!Array.isArray(node.path) ||
21
30
  !node.path.every((key) => typeof key === "string"))
22
31
  available = false;
@@ -78,8 +87,8 @@ export function conditionRequirement(rule, inputs) {
78
87
  !Array.isArray(right) &&
79
88
  left.op === "reference" &&
80
89
  right.op === "literal" &&
81
- ["gte", "lt", "eq"].includes(String(rule.op))) {
82
- return `${rule.op === "gte" ? "At least" : rule.op === "lt" ? "Less than" : "Equal to"} ${formatConditionLiteral(right.value, left.probability === true)}`;
90
+ ["gte", "lt", "gt", "eq"].includes(String(rule.op))) {
91
+ return `${rule.op === "gte" ? "At least" : rule.op === "lt" ? "Less than" : rule.op === "gt" ? "Greater than" : "Equal to"} ${formatConditionLiteral(right.value, left.probability === true)}`;
83
92
  }
84
93
  }
85
94
  const rename = (node, depth = 0) => {
@@ -89,7 +98,7 @@ export function conditionRequirement(rule, inputs) {
89
98
  return {
90
99
  ...node,
91
100
  label: typeof node.label === "string"
92
- ? (names.get(node.label) ?? "Input")
101
+ ? names.get(node.label) ?? "Input"
93
102
  : "Input",
94
103
  };
95
104
  if (Array.isArray(node.args))
@@ -30,7 +30,8 @@ export function formatFlowCondition(value, depth = 0) {
30
30
  : "FALSE (no conditions)";
31
31
  return `(${args.map(format).join(value.op === "all" ? " AND " : " OR ")})`;
32
32
  }
33
- if (!["eq", "lt", "gte"].includes(String(value.op)) || args.length !== 2)
33
+ if (!["eq", "lt", "gt", "gte"].includes(String(value.op)) ||
34
+ args.length !== 2)
34
35
  return "[rule unavailable]";
35
36
  const probability = args.some((arg) => arg &&
36
37
  typeof arg === "object" &&
@@ -43,5 +44,5 @@ export function formatFlowCondition(value, depth = 0) {
43
44
  arg.op === "literal"
44
45
  ? formatConditionLiteral(arg.value, probability)
45
46
  : format(arg);
46
- return `${operand(args[0])} ${value.op === "eq" ? "=" : value.op === "lt" ? "<" : "≥"} ${operand(args[1])}`;
47
+ return `${operand(args[0])} ${value.op === "eq" ? "=" : value.op === "lt" ? "<" : value.op === "gt" ? ">" : "≥"} ${operand(args[1])}`;
47
48
  }