jev-devtools 0.2.0 → 0.4.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.
- package/README.md +50 -6
- package/lib/flow/index.d.ts +143 -0
- package/lib/flow/index.js +1029 -0
- package/lib/flow/runtime.d.ts +5 -0
- package/lib/flow/runtime.js +81 -0
- package/lib/server/flows.d.ts +28 -0
- package/lib/server/flows.js +187 -0
- package/lib/server/index.d.ts +14 -1
- package/lib/server/index.js +100 -1
- package/lib/server/recorder.d.ts +4 -1
- package/lib/server/recorder.js +19 -3
- package/lib/src/ChoiceProbabilities.js +1 -1
- package/lib/src/ConnectedInspector.js +13 -4
- package/lib/src/DiagramContent.d.ts +6 -0
- package/lib/src/DiagramContent.js +32 -0
- package/lib/src/FlowDetailsPanel.d.ts +8 -0
- package/lib/src/FlowDetailsPanel.js +79 -0
- package/lib/src/FlowOverview.d.ts +8 -0
- package/lib/src/FlowOverview.js +440 -0
- package/lib/src/JevDevtools.d.ts +6 -1
- package/lib/src/JevDevtools.js +18 -4
- package/lib/src/TraceDiagram.d.ts +4 -1
- package/lib/src/TraceDiagram.js +30 -12
- package/lib/src/condition-inputs.d.ts +9 -0
- package/lib/src/condition-inputs.js +109 -0
- package/lib/src/flow-condition.d.ts +4 -0
- package/lib/src/flow-condition.js +48 -0
- package/lib/src/flow-model.d.ts +7 -2
- package/lib/src/flow-model.js +2 -11
- package/lib/src/flow-protocol.d.ts +93 -0
- package/lib/src/flow-protocol.js +1 -0
- package/lib/src/flow-stream.d.ts +8 -0
- package/lib/src/flow-stream.js +80 -0
- package/lib/src/inspector-styles.js +1 -1
- package/lib/src/inspector.css +746 -8
- package/lib/src/protocol.d.ts +5 -0
- package/lib/src/useDiagramViewport.d.ts +5 -1
- package/lib/src/useDiagramViewport.js +31 -9
- package/package.json +11 -4
- package/skills/setup-jev-devtools/SKILL.md +8 -4
- package/skills/setup-jev-devtools/references/flows.md +227 -0
- package/skills/setup-jev-devtools/references/nextjs.md +3 -1
- package/skills/setup-jev-devtools/references/react-node.md +2 -2
- package/skills/setup-jev-devtools/references/verify.md +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useMemo, useState } from "react";
|
|
3
|
+
const PAGE_SIZE = 12;
|
|
4
|
+
function TextValue({ value }) {
|
|
5
|
+
const [expanded, setExpanded] = useState(false);
|
|
6
|
+
const long = value.length > 140 || value.split("\n").length > 3;
|
|
7
|
+
return (_jsxs("span", { className: "diagram-text-value", children: [_jsx("span", { className: long && !expanded ? "diagram-text-preview" : undefined, children: value === "" ? (_jsx("span", { className: "diagram-value-empty", children: "empty string" })) : long && !expanded ? (`${value.slice(0, 140)}${value.length > 140 ? "…" : ""}`) : (value) }), long && (_jsx("button", { type: "button", className: "diagram-content-toggle", "aria-expanded": expanded, onClick: () => setExpanded(!expanded), children: expanded ? "Less text" : "Full text" }))] }));
|
|
8
|
+
}
|
|
9
|
+
function Properties({ value }) {
|
|
10
|
+
const [visible, setVisible] = useState(PAGE_SIZE);
|
|
11
|
+
const entries = useMemo(() => Object.entries(value), [value]);
|
|
12
|
+
if (!entries.length)
|
|
13
|
+
return (_jsx("span", { className: "diagram-value-empty", children: Array.isArray(value) ? "[] Empty array" : "{} Empty object" }));
|
|
14
|
+
return (_jsxs("div", { className: "diagram-properties", children: [entries.slice(0, visible).map(([name, child]) => (_jsx(Property, { name: name, value: child }, name))), visible < entries.length && (_jsxs("button", { type: "button", className: "diagram-content-toggle", onClick: () => setVisible((count) => count + PAGE_SIZE), children: ["Show ", Math.min(PAGE_SIZE, entries.length - visible), " more (", entries.length - visible, " remaining)"] })), visible > PAGE_SIZE && (_jsx("button", { type: "button", className: "diagram-content-toggle", onClick: () => setVisible(PAGE_SIZE), children: "Fewer properties" }))] }));
|
|
15
|
+
}
|
|
16
|
+
function Scalar({ value }) {
|
|
17
|
+
if (typeof value === "string")
|
|
18
|
+
return _jsx(TextValue, { value: value });
|
|
19
|
+
return (_jsx("span", { className: `diagram-value-${value === null || value === undefined ? "empty" : typeof value}`, children: value === undefined ? "Not captured" : String(value) }));
|
|
20
|
+
}
|
|
21
|
+
function Property({ name, value }) {
|
|
22
|
+
const [expanded, setExpanded] = useState(false);
|
|
23
|
+
const object = value !== null && typeof value === "object";
|
|
24
|
+
if (!object)
|
|
25
|
+
return (_jsxs("div", { className: `diagram-property ${typeof value === "string" ? "diagram-property-text" : ""}`, children: [_jsx("span", { className: "diagram-property-key", children: name || '""' }), _jsx(Scalar, { value: value })] }));
|
|
26
|
+
const count = Object.keys(value).length;
|
|
27
|
+
return (_jsxs("div", { className: "diagram-property-group", children: [_jsxs("button", { type: "button", className: "diagram-property-disclosure", "aria-label": `${expanded ? "Collapse" : "Expand"} ${name || '""'}`, "aria-expanded": expanded, onClick: () => setExpanded(!expanded), children: [_jsx("span", { "aria-hidden": "true", className: "diagram-property-caret", children: expanded ? "▾" : "▸" }), _jsx("span", { className: "diagram-property-key", children: name || '""' }), _jsx("span", { className: "diagram-property-type", children: Array.isArray(value) ? `[${count}]` : `{${count}}` })] }), expanded && (_jsx("div", { className: "diagram-property-children", children: _jsx(Properties, { value: value }) }))] }));
|
|
28
|
+
}
|
|
29
|
+
/** Bounded, lazy property rendering inside a fixed-size diagram card. */
|
|
30
|
+
export const DiagramContent = memo(function DiagramContent({ value, label, }) {
|
|
31
|
+
return (_jsx("div", { className: "diagram-content", "data-diagram-scroll": true, role: "region", "aria-label": label, tabIndex: 0, children: value !== null && typeof value === "object" ? (_jsx(Properties, { value: value })) : (_jsx(Scalar, { value: value })) }));
|
|
32
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/** Keep metadata out of the canvas until requested; size it within the flow workspace. */
|
|
3
|
+
export declare function FlowDetailsPanel({ children, selection, onLayoutChange, inline, }: {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
selection?: string;
|
|
6
|
+
onLayoutChange: () => void;
|
|
7
|
+
inline?: boolean;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
3
|
+
/** Keep metadata out of the canvas until requested; size it within the flow workspace. */
|
|
4
|
+
export function FlowDetailsPanel({ children, selection, onLayoutChange, inline = false, }) {
|
|
5
|
+
const id = useId();
|
|
6
|
+
const panel = useRef(null);
|
|
7
|
+
const drag = useRef(null);
|
|
8
|
+
const [open, setOpen] = useState(false);
|
|
9
|
+
const [height, setHeight] = useState(240);
|
|
10
|
+
const [max, setMax] = useState(0);
|
|
11
|
+
const min = Math.min(96, max);
|
|
12
|
+
const visibleHeight = Math.max(min, Math.min(max, height));
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const workspace = panel.current?.parentElement;
|
|
15
|
+
if (!workspace)
|
|
16
|
+
return;
|
|
17
|
+
const measure = () => {
|
|
18
|
+
// Inline panels grow the canvas instead of taking space from a fixed viewport.
|
|
19
|
+
// Exclude the panel itself so resizing cannot grow its own upper bound.
|
|
20
|
+
const available = workspace.clientHeight -
|
|
21
|
+
(inline ? panel.current?.offsetHeight ?? 0 : 0);
|
|
22
|
+
// Leave room for the bar/divider and a usable canvas, including short drawers.
|
|
23
|
+
setMax(Math.max(0, available - Math.min(140, available * 0.45) - 44));
|
|
24
|
+
};
|
|
25
|
+
measure();
|
|
26
|
+
const observer = new ResizeObserver(measure);
|
|
27
|
+
observer.observe(workspace);
|
|
28
|
+
return () => observer.disconnect();
|
|
29
|
+
}, [inline]);
|
|
30
|
+
const resize = (next) => {
|
|
31
|
+
onLayoutChange();
|
|
32
|
+
setHeight(Math.max(min, Math.min(max, next)));
|
|
33
|
+
};
|
|
34
|
+
return (_jsxs("div", { className: "flow-details-panel", ref: panel, "data-diagram-scroll": true, children: [open && (_jsx("div", { className: "flow-details-resize", role: "separator", tabIndex: 0, "aria-label": "Resize flow details", "aria-orientation": "horizontal", "aria-controls": id, "aria-valuemin": Math.round(min), "aria-valuemax": Math.round(max), "aria-valuenow": Math.round(visibleHeight), "aria-valuetext": `${Math.round(visibleHeight)} pixels high`, onPointerDown: (event) => {
|
|
35
|
+
if (event.button !== 0)
|
|
36
|
+
return;
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
event.currentTarget.focus({ preventScroll: true });
|
|
39
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
40
|
+
drag.current = {
|
|
41
|
+
y: event.clientY,
|
|
42
|
+
height: visibleHeight,
|
|
43
|
+
// Inline item details share the outer canvas zoom.
|
|
44
|
+
scale: event.currentTarget.getBoundingClientRect().height /
|
|
45
|
+
event.currentTarget.offsetHeight || 1,
|
|
46
|
+
};
|
|
47
|
+
}, onPointerMove: (event) => {
|
|
48
|
+
if (drag.current)
|
|
49
|
+
resize(drag.current.height +
|
|
50
|
+
(drag.current.y - event.clientY) / drag.current.scale);
|
|
51
|
+
}, onPointerUp: (event) => {
|
|
52
|
+
drag.current = null;
|
|
53
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId))
|
|
54
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
55
|
+
}, onPointerCancel: () => {
|
|
56
|
+
drag.current = null;
|
|
57
|
+
}, onLostPointerCapture: () => {
|
|
58
|
+
drag.current = null;
|
|
59
|
+
}, onKeyDown: (event) => {
|
|
60
|
+
const increment = event.shiftKey ? 80 : 20;
|
|
61
|
+
const next = event.key === "ArrowUp"
|
|
62
|
+
? visibleHeight + increment
|
|
63
|
+
: event.key === "ArrowDown"
|
|
64
|
+
? visibleHeight - increment
|
|
65
|
+
: event.key === "Home"
|
|
66
|
+
? min
|
|
67
|
+
: event.key === "End"
|
|
68
|
+
? max
|
|
69
|
+
: undefined;
|
|
70
|
+
if (next !== undefined) {
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
event.stopPropagation();
|
|
73
|
+
resize(next);
|
|
74
|
+
}
|
|
75
|
+
}, children: _jsx("span", {}) })), _jsxs("div", { className: "flow-details-bar", children: [_jsx("button", { "aria-expanded": open, "aria-controls": id, onClick: () => {
|
|
76
|
+
onLayoutChange();
|
|
77
|
+
setOpen(!open);
|
|
78
|
+
}, children: open ? "Hide details" : "Show details" }), selection && _jsx("span", { title: selection, children: selection })] }), _jsx("div", { id: id, hidden: !open, className: "flow-details-body", role: "region", "aria-label": "Flow details", tabIndex: 0, style: { height: visibleHeight }, children: open && children })] }));
|
|
79
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FlowDefinition, FlowRun } from "./flow-protocol.js";
|
|
2
|
+
import type { Trace } from "./protocol.js";
|
|
3
|
+
export declare const FlowOverview: import("react").MemoExoticComponent<({ definition, run, traces, inline, }: {
|
|
4
|
+
definition: FlowDefinition;
|
|
5
|
+
run: FlowRun;
|
|
6
|
+
traces: Trace[];
|
|
7
|
+
inline?: boolean;
|
|
8
|
+
}) => import("react").JSX.Element>;
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useDiagramViewport } from "./useDiagramViewport.js";
|
|
4
|
+
import { RequestDetail } from "./JevDevtools.js";
|
|
5
|
+
import { JsonBlock } from "./TraceContent.js";
|
|
6
|
+
import { conditionInputs, conditionRequirement, } from "./condition-inputs.js";
|
|
7
|
+
import { formatFlowCondition } from "./flow-condition.js";
|
|
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
|
+
}
|
|
34
|
+
const RequestNode = memo(function RequestNode({ node, trace, status, selected, x, y, onSelect, onResize, }) {
|
|
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
|
|
37
|
+
? ` · ${Math.round(trace.durationMs)} ms`
|
|
38
|
+
: ""] })] }), _jsx(TraceDiagram, { trace: trace, embedded: true })] }));
|
|
39
|
+
});
|
|
40
|
+
function joinTitle(count) {
|
|
41
|
+
return count === 2 ? "Wait for both checks" : `Wait for all ${count} paths`;
|
|
42
|
+
}
|
|
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);
|
|
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;
|
|
49
|
+
const finished = lanes.filter((lane) => lane.status === "completed").length;
|
|
50
|
+
const route = [...node.path]
|
|
51
|
+
.reverse()
|
|
52
|
+
.find((part) => part.endsWith(":then") || part.endsWith(":else"));
|
|
53
|
+
const stateLabel = status === "untaken"
|
|
54
|
+
? "Not taken"
|
|
55
|
+
: node.kind === "output" && status === "completed"
|
|
56
|
+
? "Returned outcome"
|
|
57
|
+
: status;
|
|
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
|
|
79
|
+
: decision !== undefined
|
|
80
|
+
? decision
|
|
81
|
+
? "Condition met · then"
|
|
82
|
+
: "Condition not met · else"
|
|
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" })] }))] }));
|
|
123
|
+
});
|
|
124
|
+
export const FlowOverview = memo(function FlowOverview({ definition, run, traces, inline = false, }) {
|
|
125
|
+
const [selected, setSelected] = useState(() => definition.nodes.find((n) => n.kind === "jev")?.id ?? "");
|
|
126
|
+
const [details, setDetails] = useState(false);
|
|
127
|
+
const [collectionViews, setCollectionViews] = useState({});
|
|
128
|
+
const [itemSizes, setItemSizes] = useState({});
|
|
129
|
+
const [heights, setHeights] = useState({});
|
|
130
|
+
const tracesById = useMemo(() => new Map(traces.map((trace) => [trace.id, trace])), [traces]);
|
|
131
|
+
const inputsByStep = useMemo(() => new Map(definition.nodes
|
|
132
|
+
.filter((node) => node.conditionRule !== undefined)
|
|
133
|
+
.map((node) => [
|
|
134
|
+
node.id,
|
|
135
|
+
conditionInputs(node.conditionRule, run, tracesById, new Map(definition.nodes.map((n) => [n.id, n.title ?? n.id]))),
|
|
136
|
+
])), [definition, run, tracesById]);
|
|
137
|
+
// Omit redundant transitive edges so a reference does not draw through an
|
|
138
|
+
// intervening condition. Execution ordering still includes every dependency.
|
|
139
|
+
const links = useMemo(() => {
|
|
140
|
+
const byId = new Map(definition.nodes.map((node) => [node.id, node]));
|
|
141
|
+
const ancestors = (id) => {
|
|
142
|
+
const seen = new Set(), pending = [...(byId.get(id)?.dependencies ?? [])];
|
|
143
|
+
while (pending.length) {
|
|
144
|
+
const next = pending.pop();
|
|
145
|
+
if (seen.has(next))
|
|
146
|
+
continue;
|
|
147
|
+
seen.add(next);
|
|
148
|
+
pending.push(...(byId.get(next)?.dependencies ?? []));
|
|
149
|
+
}
|
|
150
|
+
return seen;
|
|
151
|
+
};
|
|
152
|
+
const parents = new Map(definition.nodes.map((node) => [node.id, ancestors(node.id)]));
|
|
153
|
+
return new Map(definition.nodes.map((node) => [
|
|
154
|
+
node.id,
|
|
155
|
+
node.dependencies.filter((id) => node.kind === "merge" ||
|
|
156
|
+
!node.dependencies.some((other) => other !== id && parents.get(other)?.has(id))),
|
|
157
|
+
]));
|
|
158
|
+
}, [definition]);
|
|
159
|
+
const parallelLanes = useMemo(() => new Map(definition.nodes
|
|
160
|
+
.filter((n) => n.kind === "parallel")
|
|
161
|
+
.map((node) => [
|
|
162
|
+
node.id,
|
|
163
|
+
(node.lanes ?? node.dependencies).map((name) => {
|
|
164
|
+
const children = node.lanes
|
|
165
|
+
? definition.nodes.filter((child) => child.path.includes(`${node.id}:${name}`))
|
|
166
|
+
: definition.nodes.filter((child) => child.id === name);
|
|
167
|
+
const states = children.map((child) => run.steps[child.id]?.status ?? "waiting");
|
|
168
|
+
const status = run.steps[node.id]?.status === "untaken"
|
|
169
|
+
? "untaken"
|
|
170
|
+
: states.includes("error")
|
|
171
|
+
? "error"
|
|
172
|
+
: states.includes("cancelled")
|
|
173
|
+
? "cancelled"
|
|
174
|
+
: states.includes("blocked")
|
|
175
|
+
? "blocked"
|
|
176
|
+
: run.steps[node.id]?.status === "completed" ||
|
|
177
|
+
(states.length > 0 &&
|
|
178
|
+
states.every((state) => state === "completed" || state === "untaken"))
|
|
179
|
+
? "completed"
|
|
180
|
+
: states.some((state) => state === "running" || state === "completed")
|
|
181
|
+
? "running"
|
|
182
|
+
: "waiting";
|
|
183
|
+
return { name, status };
|
|
184
|
+
}),
|
|
185
|
+
])), [definition, run.steps]);
|
|
186
|
+
const layout = useMemo(() => {
|
|
187
|
+
const columns = new Map();
|
|
188
|
+
let remaining = [...definition.nodes];
|
|
189
|
+
while (remaining.length) {
|
|
190
|
+
const ready = remaining.filter((node) => node.dependencies.every((id) => columns.has(id)));
|
|
191
|
+
if (!ready.length)
|
|
192
|
+
break;
|
|
193
|
+
for (const node of ready)
|
|
194
|
+
columns.set(node.id, node.dependencies.length
|
|
195
|
+
? Math.max(...node.dependencies.map((id) => columns.get(id))) + 1
|
|
196
|
+
: 0);
|
|
197
|
+
const ids = new Set(ready.map((node) => node.id));
|
|
198
|
+
remaining = remaining.filter((node) => !ids.has(node.id));
|
|
199
|
+
}
|
|
200
|
+
const points = new Map();
|
|
201
|
+
const expansions = new Map();
|
|
202
|
+
let x = 40;
|
|
203
|
+
for (let column = 0; column <= Math.max(0, ...columns.values()); column++) {
|
|
204
|
+
const nodes = definition.nodes.filter((node) => columns.get(node.id) === column);
|
|
205
|
+
let bottom = 40, width = 280;
|
|
206
|
+
for (const node of nodes) {
|
|
207
|
+
const captured = node.kind === "jev" &&
|
|
208
|
+
tracesById.has(run.steps[node.id]?.traceId ?? "");
|
|
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));
|
|
223
|
+
const parents = (links.get(node.id) ?? [])
|
|
224
|
+
.map((id) => points.get(id))
|
|
225
|
+
.filter(Boolean);
|
|
226
|
+
const center = parents.length
|
|
227
|
+
? parents.reduce((sum, p) => sum + p.y + p.height / 2, 0) /
|
|
228
|
+
parents.length
|
|
229
|
+
: h / 2 + 40;
|
|
230
|
+
const y = Math.max(bottom, center - h / 2);
|
|
231
|
+
points.set(node.id, { x, y, width: w, height: h });
|
|
232
|
+
bottom = y + h + 100;
|
|
233
|
+
width = Math.max(width, w);
|
|
234
|
+
}
|
|
235
|
+
x += width + 160;
|
|
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));
|
|
263
|
+
return {
|
|
264
|
+
points,
|
|
265
|
+
expansions,
|
|
266
|
+
left,
|
|
267
|
+
width: right - left,
|
|
268
|
+
height: Math.max(400, ...[...points.values(), ...expansions.values()].map((p) => p.y + p.height + 40)),
|
|
269
|
+
};
|
|
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
|
+
});
|
|
284
|
+
const onResize = useCallback((id, height) => {
|
|
285
|
+
preserveCamera();
|
|
286
|
+
setHeights((previous) => previous[id] === height ? previous : { ...previous, [id]: height });
|
|
287
|
+
}, [preserveCamera]);
|
|
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]);
|
|
302
|
+
useEffect(() => {
|
|
303
|
+
if (inline || started.current)
|
|
304
|
+
return;
|
|
305
|
+
const first = layout.points.get(selected);
|
|
306
|
+
if (first) {
|
|
307
|
+
started.current = true;
|
|
308
|
+
focusArea(first);
|
|
309
|
+
}
|
|
310
|
+
}, [layout, selected, focusArea, inline]);
|
|
311
|
+
const selectStep = (id) => {
|
|
312
|
+
setSelected(id);
|
|
313
|
+
const bounds = layout.points.get(id);
|
|
314
|
+
if (bounds && !inline)
|
|
315
|
+
focusArea(bounds);
|
|
316
|
+
};
|
|
317
|
+
const node = definition.nodes.find((n) => n.id === selected);
|
|
318
|
+
const step = node ? run.steps[node.id] : undefined;
|
|
319
|
+
const trace = step?.traceId
|
|
320
|
+
? traces.find((t) => t.id === step.traceId)
|
|
321
|
+
: undefined;
|
|
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" }) }) })] }));
|
|
440
|
+
});
|
package/lib/src/JevDevtools.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { type FlowHistory } from "./flow-stream.js";
|
|
1
2
|
import type { Trace } from "./protocol.js";
|
|
2
|
-
export declare
|
|
3
|
+
export declare const RequestDetail: import("react").MemoExoticComponent<({ trace, }: {
|
|
4
|
+
trace?: Trace;
|
|
5
|
+
}) => import("react").JSX.Element>;
|
|
6
|
+
export declare function JevPanel({ traces, flows, connection, onClear, }: {
|
|
3
7
|
traces: Trace[];
|
|
8
|
+
flows?: FlowHistory;
|
|
4
9
|
connection: string;
|
|
5
10
|
onClear: () => Promise<unknown>;
|
|
6
11
|
}): import("react").JSX.Element;
|