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.
- package/README.md +11 -12
- package/lib/flow/index.d.ts +64 -2
- package/lib/flow/index.js +466 -25
- package/lib/flow/runtime.d.ts +5 -0
- package/lib/flow/runtime.js +81 -0
- package/lib/server/flows.d.ts +3 -0
- package/lib/server/flows.js +89 -17
- package/lib/server/index.d.ts +6 -1
- package/lib/server/index.js +15 -0
- package/lib/server/recorder.js +7 -2
- package/lib/src/FlowDetailsPanel.d.ts +8 -0
- package/lib/src/FlowDetailsPanel.js +79 -0
- package/lib/src/FlowOverview.d.ts +2 -1
- package/lib/src/FlowOverview.js +292 -80
- package/lib/src/JevDrawer.js +2 -4
- package/lib/src/condition-inputs.js +12 -3
- package/lib/src/flow-condition.js +3 -2
- package/lib/src/flow-protocol.d.ts +27 -2
- package/lib/src/inspector-styles.js +1 -1
- package/lib/src/inspector.css +371 -260
- package/lib/src/useDiagramViewport.d.ts +1 -1
- package/lib/src/useDiagramViewport.js +11 -6
- package/package.json +6 -3
- package/skills/setup-jev-devtools/SKILL.md +7 -7
- package/skills/setup-jev-devtools/references/flows.md +86 -9
- package/skills/setup-jev-devtools/references/nextjs.md +3 -1
- package/skills/setup-jev-devtools/references/react-node.md +3 -4
- package/skills/setup-jev-devtools/references/verify.md +1 -1
package/lib/src/FlowOverview.js
CHANGED
|
@@ -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
|
|
11
|
-
|
|
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
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 })] })
|
|
61
|
-
|
|
62
|
-
|
|
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) =>
|
|
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 =
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
(node.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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
|
-
}, [
|
|
176
|
-
|
|
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] })) : (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
});
|
package/lib/src/JevDrawer.js
CHANGED
|
@@ -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
|
|
54
|
-
//
|
|
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
|
-
?
|
|
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)) ||
|
|
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
|
}
|