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.
Files changed (44) hide show
  1. package/README.md +50 -6
  2. package/lib/flow/index.d.ts +143 -0
  3. package/lib/flow/index.js +1029 -0
  4. package/lib/flow/runtime.d.ts +5 -0
  5. package/lib/flow/runtime.js +81 -0
  6. package/lib/server/flows.d.ts +28 -0
  7. package/lib/server/flows.js +187 -0
  8. package/lib/server/index.d.ts +14 -1
  9. package/lib/server/index.js +100 -1
  10. package/lib/server/recorder.d.ts +4 -1
  11. package/lib/server/recorder.js +19 -3
  12. package/lib/src/ChoiceProbabilities.js +1 -1
  13. package/lib/src/ConnectedInspector.js +13 -4
  14. package/lib/src/DiagramContent.d.ts +6 -0
  15. package/lib/src/DiagramContent.js +32 -0
  16. package/lib/src/FlowDetailsPanel.d.ts +8 -0
  17. package/lib/src/FlowDetailsPanel.js +79 -0
  18. package/lib/src/FlowOverview.d.ts +8 -0
  19. package/lib/src/FlowOverview.js +440 -0
  20. package/lib/src/JevDevtools.d.ts +6 -1
  21. package/lib/src/JevDevtools.js +18 -4
  22. package/lib/src/TraceDiagram.d.ts +4 -1
  23. package/lib/src/TraceDiagram.js +30 -12
  24. package/lib/src/condition-inputs.d.ts +9 -0
  25. package/lib/src/condition-inputs.js +109 -0
  26. package/lib/src/flow-condition.d.ts +4 -0
  27. package/lib/src/flow-condition.js +48 -0
  28. package/lib/src/flow-model.d.ts +7 -2
  29. package/lib/src/flow-model.js +2 -11
  30. package/lib/src/flow-protocol.d.ts +93 -0
  31. package/lib/src/flow-protocol.js +1 -0
  32. package/lib/src/flow-stream.d.ts +8 -0
  33. package/lib/src/flow-stream.js +80 -0
  34. package/lib/src/inspector-styles.js +1 -1
  35. package/lib/src/inspector.css +746 -8
  36. package/lib/src/protocol.d.ts +5 -0
  37. package/lib/src/useDiagramViewport.d.ts +5 -1
  38. package/lib/src/useDiagramViewport.js +31 -9
  39. package/package.json +11 -4
  40. package/skills/setup-jev-devtools/SKILL.md +8 -4
  41. package/skills/setup-jev-devtools/references/flows.md +227 -0
  42. package/skills/setup-jev-devtools/references/nextjs.md +3 -1
  43. package/skills/setup-jev-devtools/references/react-node.md +2 -2
  44. 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
+ });
@@ -1,6 +1,11 @@
1
+ import { type FlowHistory } from "./flow-stream.js";
1
2
  import type { Trace } from "./protocol.js";
2
- export declare function JevPanel({ traces, connection, onClear, }: {
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;