jev-devtools 0.1.3 → 0.3.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 (52) hide show
  1. package/README.md +50 -4
  2. package/THIRD_PARTY_NOTICES.txt +27 -0
  3. package/lib/flow/index.d.ts +82 -0
  4. package/lib/flow/index.js +588 -0
  5. package/lib/server/flows.d.ts +25 -0
  6. package/lib/server/flows.js +115 -0
  7. package/lib/server/index.d.ts +8 -0
  8. package/lib/server/index.js +163 -9
  9. package/lib/server/recorder.d.ts +12 -2
  10. package/lib/server/recorder.js +47 -9
  11. package/lib/src/ChoiceProbabilities.d.ts +4 -0
  12. package/lib/src/ChoiceProbabilities.js +19 -0
  13. package/lib/src/ConnectedInspector.js +46 -29
  14. package/lib/src/DiagramContent.d.ts +6 -0
  15. package/lib/src/DiagramContent.js +32 -0
  16. package/lib/src/FlowOverview.d.ts +7 -0
  17. package/lib/src/FlowOverview.js +228 -0
  18. package/lib/src/JevDevtools.d.ts +6 -1
  19. package/lib/src/JevDevtools.js +52 -48
  20. package/lib/src/JevDrawer.d.ts +3 -2
  21. package/lib/src/JevDrawer.js +12 -3
  22. package/lib/src/Questions.d.ts +5 -0
  23. package/lib/src/Questions.js +174 -0
  24. package/lib/src/TraceContent.d.ts +11 -0
  25. package/lib/src/TraceContent.js +32 -0
  26. package/lib/src/TraceDiagram.d.ts +4 -1
  27. package/lib/src/TraceDiagram.js +111 -25
  28. package/lib/src/condition-inputs.d.ts +9 -0
  29. package/lib/src/condition-inputs.js +100 -0
  30. package/lib/src/flow-condition.d.ts +4 -0
  31. package/lib/src/flow-condition.js +47 -0
  32. package/lib/src/flow-model.d.ts +7 -2
  33. package/lib/src/flow-model.js +3 -12
  34. package/lib/src/flow-protocol.d.ts +68 -0
  35. package/lib/src/flow-protocol.js +1 -0
  36. package/lib/src/flow-stream.d.ts +8 -0
  37. package/lib/src/flow-stream.js +80 -0
  38. package/lib/src/index.d.ts +1 -1
  39. package/lib/src/inspector-styles.js +1 -1
  40. package/lib/src/inspector.css +725 -8
  41. package/lib/src/protocol.d.ts +18 -0
  42. package/lib/src/trace-store.d.ts +6 -0
  43. package/lib/src/trace-store.js +66 -0
  44. package/lib/src/trace-stream.d.ts +2 -0
  45. package/lib/src/trace-stream.js +65 -0
  46. package/lib/src/useDiagramViewport.d.ts +5 -0
  47. package/lib/src/useDiagramViewport.js +34 -6
  48. package/package.json +7 -2
  49. package/skills/setup-jev-devtools/SKILL.md +6 -2
  50. package/skills/setup-jev-devtools/references/flows.md +150 -0
  51. package/skills/setup-jev-devtools/references/nextjs.md +1 -1
  52. package/skills/setup-jev-devtools/references/verify.md +3 -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,7 @@
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, }: {
4
+ definition: FlowDefinition;
5
+ run: FlowRun;
6
+ traces: Trace[];
7
+ }) => import("react").JSX.Element>;
@@ -0,0 +1,228 @@
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
+ const RequestNode = memo(function RequestNode({ node, trace, status, selected, x, y, onSelect, onResize, }) {
10
+ const headingHeight = node.description ? 104 : 64;
11
+ const measure = useCallback((height) => onResize(node.id, height + headingHeight), [node.id, onResize, headingHeight]);
12
+ return (_jsxs("article", { className: `flow-step flow-request ${selected ? "selected" : ""}`, "data-status": status, "aria-label": `Request ${node.id}`, style: { left: x, top: y, width: 1120 }, children: [_jsxs("button", { className: "flow-request-heading", style: { height: headingHeight }, "aria-label": `Step ${node.id}, jev, ${status}`, "aria-pressed": selected, onClick: () => onSelect(node.id), children: [_jsxs("span", { children: [_jsx("span", { className: "eyebrow", children: "Jev request" }), _jsx("strong", { children: node.title ?? node.id }), node.description && (_jsx("span", { className: "flow-request-description", children: node.description }))] }), _jsxs("span", { children: [status, trace.durationMs !== undefined
13
+ ? ` · ${Math.round(trace.durationMs)} ms`
14
+ : ""] })] }), _jsx(TraceDiagram, { trace: trace, embedded: true, onHeightChange: measure })] }));
15
+ });
16
+ function joinTitle(count) {
17
+ return count === 2 ? "Wait for both checks" : `Wait for all ${count} paths`;
18
+ }
19
+ const Card = memo(function Card({ node, status, decision, selected, bounds, onSelect, lanes, inputs, }) {
20
+ const join = node.kind === "parallel";
21
+ const finished = lanes.filter((lane) => lane.status === "completed").length;
22
+ const route = [...node.path]
23
+ .reverse()
24
+ .find((part) => part.endsWith(":then") || part.endsWith(":else"));
25
+ const stateLabel = status === "untaken"
26
+ ? "Not taken"
27
+ : node.kind === "output" && status === "completed"
28
+ ? "Returned outcome"
29
+ : status;
30
+ return (_jsxs("button", { className: `flow-step ${selected ? "selected" : ""}`, "data-status": status, style: {
31
+ left: bounds.x,
32
+ top: bounds.y,
33
+ width: bounds.width,
34
+ height: bounds.height,
35
+ }, "aria-label": `Step ${node.id}, ${node.kind}, ${status}`, "aria-pressed": selected, onClick: () => onSelect(node.id), children: [_jsxs("span", { className: "flow-card-heading", children: [_jsx("span", { className: "flow-kind-icon", "aria-hidden": "true", children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: join ? (_jsx("path", { d: "M4 5h4l5 7h7M4 19h4l5-7m4-3 3 3-3 3" })) : node.kind === "output" ? (_jsxs(_Fragment, { children: [_jsx("rect", { x: "4", y: "4", width: "16", height: "16", rx: "4" }), _jsx("path", { d: "m8 12 3 3 5-6" })] })) : (_jsx("path", { d: "m12 3 9 9-9 9-9-9zM8 12h8" })) }) }), _jsx("span", { className: "eyebrow", children: join
36
+ ? "Parallel checks"
37
+ : node.kind === "branch"
38
+ ? "Condition"
39
+ : node.kind }), node.kind === "output" && (_jsx("span", { className: "flow-status", children: stateLabel }))] }), _jsx("strong", { children: node.title ?? (join ? joinTitle(lanes.length) : node.id) }), node.description && (_jsx("span", { className: "flow-step-description", children: node.description })), node.kind === "branch" &&
40
+ (node.conditionRule !== undefined || node.condition) && (_jsxs("span", { className: "flow-condition-rule", children: [inputs && inputs.length > 0 && (_jsxs("span", { className: "flow-condition-incoming", children: [_jsx("span", { className: "flow-condition-label", children: inputs.length === 1 ? "Incoming value" : "Incoming values" }), inputs.slice(0, 3).map((input) => (_jsxs("span", { className: "flow-condition-input", children: [_jsx("span", { children: input.name }), _jsx("b", { children: input.value })] }, input.reference))), inputs.length > 3 && (_jsxs("small", { children: [inputs.length - 3, " more inputs \u00B7 select to inspect"] }))] })), _jsx("span", { className: "flow-condition-label", children: "Requirement" }), _jsx("code", { children: node.conditionRule !== undefined
41
+ ? conditionRequirement(node.conditionRule, inputs ?? [])
42
+ : node.condition })] })), join && (_jsxs(_Fragment, { children: [_jsxs("span", { className: "flow-join-summary", children: [joinTitle(lanes.length), " ", _jsxs("span", { children: [finished, "/", lanes.length] })] }), _jsxs("span", { className: "flow-lanes", children: [lanes.slice(0, 4).map((lane) => (_jsxs("span", { className: "flow-lane", "data-status": lane.status, children: [_jsxs("span", { children: [_jsx("span", { "aria-hidden": "true", children: lane.status === "completed"
43
+ ? "✓"
44
+ : lane.status === "error"
45
+ ? "!"
46
+ : "○" }), " ", lane.name] }), _jsx("small", { children: lane.status })] }, lane.name))), lanes.length > 4 && (_jsxs("small", { children: [lanes.length - 4, " more \u00B7 select to see all checks"] }))] })] })), _jsxs("span", { className: "flow-card-footer", children: [_jsx("code", { children: node.id }), _jsx("small", { children: join
47
+ ? status === "completed"
48
+ ? "All finished · continue"
49
+ : stateLabel
50
+ : node.kind === "output"
51
+ ? route?.endsWith(":then")
52
+ ? "Then route"
53
+ : route?.endsWith(":else")
54
+ ? "Else route"
55
+ : "Final outcome"
56
+ : decision !== undefined
57
+ ? decision
58
+ ? "Condition met · then"
59
+ : "Condition not met · else"
60
+ : stateLabel })] }), node.kind === "jev" && (_jsx("small", { children: status === "untaken"
61
+ ? "This request was not run."
62
+ : "Request details are not captured yet or no longer retained." }))] }));
63
+ });
64
+ export const FlowOverview = memo(function FlowOverview({ definition, run, traces, }) {
65
+ const [selected, setSelected] = useState(() => definition.nodes.find((n) => n.kind === "jev")?.id ?? "");
66
+ const [details, setDetails] = useState(false);
67
+ const [heights, setHeights] = useState({});
68
+ const tracesById = useMemo(() => new Map(traces.map((trace) => [trace.id, trace])), [traces]);
69
+ const inputsByStep = useMemo(() => new Map(definition.nodes
70
+ .filter((node) => node.conditionRule !== undefined)
71
+ .map((node) => [
72
+ node.id,
73
+ conditionInputs(node.conditionRule, run, tracesById, new Map(definition.nodes.map((n) => [n.id, n.title ?? n.id]))),
74
+ ])), [definition, run, tracesById]);
75
+ // Omit redundant transitive edges so a reference does not draw through an
76
+ // intervening condition. Execution ordering still includes every dependency.
77
+ const links = useMemo(() => {
78
+ const byId = new Map(definition.nodes.map((node) => [node.id, node]));
79
+ const ancestors = (id) => {
80
+ const seen = new Set(), pending = [...(byId.get(id)?.dependencies ?? [])];
81
+ while (pending.length) {
82
+ const next = pending.pop();
83
+ if (seen.has(next))
84
+ continue;
85
+ seen.add(next);
86
+ pending.push(...(byId.get(next)?.dependencies ?? []));
87
+ }
88
+ return seen;
89
+ };
90
+ const parents = new Map(definition.nodes.map((node) => [node.id, ancestors(node.id)]));
91
+ return new Map(definition.nodes.map((node) => [
92
+ node.id,
93
+ node.dependencies.filter((id) => !node.dependencies.some((other) => other !== id && parents.get(other)?.has(id))),
94
+ ]));
95
+ }, [definition]);
96
+ const parallelLanes = useMemo(() => new Map(definition.nodes
97
+ .filter((n) => n.kind === "parallel")
98
+ .map((node) => [
99
+ node.id,
100
+ (node.lanes ?? node.dependencies).map((name) => {
101
+ const children = node.lanes
102
+ ? definition.nodes.filter((child) => child.path.includes(`${node.id}:${name}`))
103
+ : definition.nodes.filter((child) => child.id === name);
104
+ const states = children.map((child) => run.steps[child.id]?.status ?? "waiting");
105
+ const status = run.steps[node.id]?.status === "untaken"
106
+ ? "untaken"
107
+ : states.includes("error")
108
+ ? "error"
109
+ : states.includes("cancelled")
110
+ ? "cancelled"
111
+ : states.includes("blocked")
112
+ ? "blocked"
113
+ : run.steps[node.id]?.status === "completed" ||
114
+ (states.length > 0 &&
115
+ states.every((state) => state === "completed" || state === "untaken"))
116
+ ? "completed"
117
+ : states.some((state) => state === "running" || state === "completed")
118
+ ? "running"
119
+ : "waiting";
120
+ return { name, status };
121
+ }),
122
+ ])), [definition, run.steps]);
123
+ const layout = useMemo(() => {
124
+ const columns = new Map();
125
+ let remaining = [...definition.nodes];
126
+ while (remaining.length) {
127
+ const ready = remaining.filter((node) => node.dependencies.every((id) => columns.has(id)));
128
+ if (!ready.length)
129
+ break;
130
+ for (const node of ready)
131
+ columns.set(node.id, node.dependencies.length
132
+ ? Math.max(...node.dependencies.map((id) => columns.get(id))) + 1
133
+ : 0);
134
+ const ids = new Set(ready.map((node) => node.id));
135
+ remaining = remaining.filter((node) => !ids.has(node.id));
136
+ }
137
+ const points = new Map();
138
+ let x = 40;
139
+ for (let column = 0; column <= Math.max(0, ...columns.values()); column++) {
140
+ const nodes = definition.nodes.filter((node) => columns.get(node.id) === column);
141
+ let bottom = 40, width = 280;
142
+ for (const node of nodes) {
143
+ const captured = node.kind === "jev" &&
144
+ tracesById.has(run.steps[node.id]?.traceId ?? "");
145
+ const w = captured ? 1120 : 360, h = captured
146
+ ? (heights[node.id] ?? 640)
147
+ : node.kind === "parallel"
148
+ ? 190 +
149
+ (node.description ? 64 : 0) +
150
+ Math.min(4, (node.lanes ?? node.dependencies).length) * 32
151
+ : (node.description ? 244 : 172) +
152
+ (node.kind === "branch"
153
+ ? 110 +
154
+ Math.min(3, inputsByStep.get(node.id)?.length ?? 0) * 24
155
+ : 0);
156
+ const parents = (links.get(node.id) ?? [])
157
+ .map((id) => points.get(id))
158
+ .filter(Boolean);
159
+ const center = parents.length
160
+ ? parents.reduce((sum, p) => sum + p.y + p.height / 2, 0) /
161
+ parents.length
162
+ : h / 2 + 40;
163
+ const y = Math.max(bottom, center - h / 2);
164
+ points.set(node.id, { x, y, width: w, height: h });
165
+ bottom = y + h + 100;
166
+ width = Math.max(width, w);
167
+ }
168
+ x += width + 160;
169
+ }
170
+ return {
171
+ points,
172
+ width: Math.max(700, x - 120),
173
+ height: Math.max(400, ...[...points.values()].map((p) => p.y + p.height + 40)),
174
+ };
175
+ }, [definition, run.steps, tracesById, heights, links, inputsByStep]);
176
+ const { viewport, canvas, zoom, zoomBy, fit, preserveCamera, focusArea } = useDiagramViewport(layout.width, layout.height, run.id);
177
+ const onResize = useCallback((id, height) => {
178
+ preserveCamera();
179
+ setHeights((previous) => previous[id] === height ? previous : { ...previous, [id]: height });
180
+ }, [preserveCamera]);
181
+ const started = useRef(false);
182
+ useEffect(() => {
183
+ if (started.current)
184
+ return;
185
+ const first = layout.points.get(selected);
186
+ if (first) {
187
+ started.current = true;
188
+ focusArea(first);
189
+ }
190
+ }, [layout, selected, focusArea]);
191
+ const selectStep = (id) => {
192
+ setSelected(id);
193
+ const bounds = layout.points.get(id);
194
+ if (bounds)
195
+ focusArea(bounds);
196
+ };
197
+ const node = definition.nodes.find((n) => n.id === selected);
198
+ const step = node ? run.steps[node.id] : undefined;
199
+ const trace = step?.traceId
200
+ ? traces.find((t) => t.id === step.traceId)
201
+ : undefined;
202
+ return (_jsxs("section", { className: "request-detail whole-flow", "aria-label": "Whole flow", children: [_jsxs("header", { className: "detail-header", children: [_jsxs("div", { children: [_jsxs("span", { className: "eyebrow", children: ["Flow run \u00B7 ", run.status] }), _jsx("h2", { children: run.name })] }), _jsxs("div", { children: [run.outcome ? (_jsxs("strong", { children: ["Outcome: ", run.outcome] })) : ((run.error ?? "Execution paths")), _jsx("small", { className: "muted", children: run.durationMs !== undefined
203
+ ? ` · ${Math.round(run.durationMs)} ms`
204
+ : "" })] })] }), _jsxs("div", { className: "lab-flow-toolbar", children: [_jsxs("div", { children: [_jsx("button", { "aria-label": "Zoom flow out", onClick: () => zoomBy(1 / 1.2), children: "\u2212" }), _jsxs("output", { "aria-label": "Flow zoom", children: [zoom, "%"] }), _jsx("button", { "aria-label": "Zoom flow in", onClick: () => zoomBy(1.2), children: "+" }), _jsx("button", { onClick: fit, children: "Fit flow" }), _jsx("button", { disabled: !selected, onClick: () => selectStep(selected), children: "Focus step" })] }), _jsxs("label", { children: ["Step", _jsxs("select", { "aria-label": "Flow step", value: selected, onChange: (e) => selectStep(e.target.value), children: [_jsx("option", { value: "", children: "Select a step" }), definition.nodes.map((n) => (_jsxs("option", { value: n.id, children: [n.title ? `${n.title} (${n.id})` : n.id, " \u00B7", " ", run.steps[n.id]?.status ?? "waiting"] }, n.id)))] })] })] }), _jsx("div", { className: "flow-overview-viewport diagram-viewport", ref: viewport, tabIndex: 0, "aria-label": "Flow canvas", role: "region", children: _jsxs("div", { ref: canvas, className: "flow-overview-canvas", style: { width: layout.width, height: layout.height }, children: [_jsxs("svg", { width: layout.width, height: layout.height, "aria-hidden": "true", className: "flow-connections", children: [_jsx("defs", { children: _jsx("marker", { id: `arrow-${run.id}`, viewBox: "0 0 10 10", refX: "8", refY: "5", markerWidth: "5", markerHeight: "5", orient: "auto", children: _jsx("path", { d: "M0 0L10 5L0 10z", fill: "currentColor" }) }) }), definition.nodes.flatMap((n) => (links.get(n.id) ?? []).map((id) => {
205
+ const a = layout.points.get(id), b = layout.points.get(n.id);
206
+ if (!a || !b)
207
+ return null;
208
+ const active = ["running", "completed"].includes(run.steps[n.id]?.status);
209
+ return (_jsx("path", { d: `M${a.x + a.width},${a.y + a.height / 2} C${a.x + a.width + 80},${a.y + a.height / 2} ${b.x - 80},${b.y + b.height / 2} ${b.x},${b.y + b.height / 2}`, stroke: active ? "#E551BA" : "currentColor", opacity: active ? 1 : 0.35, fill: "none", strokeWidth: "2", markerEnd: `url(#arrow-${run.id})` }, `${id}-${n.id}`));
210
+ }))] }), definition.nodes.map((n) => {
211
+ const p = layout.points.get(n.id);
212
+ if (!p)
213
+ return null;
214
+ const captured = tracesById.get(run.steps[n.id]?.traceId ?? "");
215
+ return n.kind === "jev" && captured ? (_jsx(RequestNode, { node: n, trace: captured, status: run.steps[n.id]?.status ?? "waiting", selected: selected === n.id, x: p.x, y: p.y, onSelect: setSelected, onResize: onResize }, n.id)) : (_jsx(Card, { node: n, status: run.steps[n.id]?.status ?? "waiting", decision: run.steps[n.id]?.decision, selected: selected === n.id, bounds: p, onSelect: setSelected, inputs: inputsByStep.get(n.id), lanes: parallelLanes.get(n.id) ?? [] }, n.id));
216
+ })] }) }), _jsxs("div", { className: "flow-selection", children: [node && step ? (_jsxs(_Fragment, { children: [_jsxs("strong", { children: [node.title ?? node.id, " \u00B7 ", step.status] }), _jsx("span", { className: "muted", children: step.durationMs !== undefined
217
+ ? ` · ${Math.round(step.durationMs)} ms`
218
+ : "" }), node.description && _jsx("p", { children: node.description }), node.kind === "parallel" && (_jsxs("p", { children: ["These paths run at the same time:", " ", (node.lanes ?? node.dependencies).join(", "), ". The next step starts only after all finish successfully. This marker waits for their results; it does not make another Jev request."] })), node.kind === "parallel" && (_jsx("ul", { className: "flow-lane-details", children: parallelLanes.get(node.id)?.map((lane) => (_jsxs("li", { children: [lane.name, " \u00B7 ", lane.status] }, lane.name))) })), (inputsByStep.get(node.id)?.length ?? 0) > 0 && (_jsx("dl", { className: "flow-input-details", children: inputsByStep.get(node.id)?.map((input) => (_jsxs("div", { children: [_jsxs("dt", { children: [input.name, " ", _jsx("code", { children: input.reference })] }), _jsx("dd", { children: input.value })] }, input.reference))) })), (node.conditionRule !== undefined || node.condition) && (_jsxs("p", { children: [_jsx("code", { children: node.conditionRule !== undefined
219
+ ? formatFlowCondition(node.conditionRule)
220
+ : node.condition }), " ", "\u00B7", " ", step.decision === undefined
221
+ ? "Not evaluated"
222
+ : step.decision
223
+ ? "Passed · then route"
224
+ : "Not met · else route"] })), _jsx("p", { className: "muted", children: node.path.length ? node.path.join(" / ") : "Main path" }), step.error && _jsx("p", { role: "alert", children: step.error }), node.kind === "jev" &&
225
+ (trace ? (_jsx("button", { onClick: () => setDetails(!details), children: details ? "Hide request details" : "Inspect request" })) : (_jsx("p", { children: step.traceId
226
+ ? "Request capture was cleared or evicted."
227
+ : "No captured request linked yet. Use a client created by this inspector." })))] })) : (_jsx("p", { className: "muted", children: "Each request diagram shows its context, question and answer. Use its question picker for other answers. Lines between requests show execution dependencies." })), run.value !== undefined && (_jsxs("details", { children: [_jsx("summary", { children: "Captured outcome value" }), _jsx(JsonBlock, { value: run.value })] }))] }), details && trace && (_jsx("div", { className: "flow-request-detail", children: _jsx(RequestDetail, { trace: trace }) }))] }));
228
+ });
@@ -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;
@@ -1,58 +1,62 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from "react";
2
+ import { emptyFlows } from "./flow-stream.js";
3
+ import { lazy, memo, startTransition, Suspense, useMemo, useState, } from "react";
3
4
  import { TraceDiagram } from "./TraceDiagram.js";
4
- const pretty = (value) => JSON.stringify(value, null, 2);
5
+ import { JsonBlock, findImage } from "./TraceContent.js";
6
+ const FlowOverview = lazy(() => import("./FlowOverview.js").then((module) => ({
7
+ default: module.FlowOverview,
8
+ })));
9
+ let questionsLoaded = false;
10
+ const Questions = lazy(() => import("./Questions.js").then((module) => {
11
+ questionsLoaded = true;
12
+ return { default: module.Questions };
13
+ }));
5
14
  const ms = (value) => value === undefined ? "—" : `${Math.round(value)} ms`;
6
- const percent = (value) => `${Math.round(value * 100)}%`;
7
- function findImage(value) {
8
- if (!value || typeof value !== "object")
9
- return;
10
- if (!Array.isArray(value) &&
11
- typeof value.preview === "string" &&
12
- /^data:image\/(png|jpeg);base64,/.test(value.preview))
13
- return value.preview;
14
- for (const child of Object.values(value)) {
15
- const found = findImage(child);
16
- if (found)
17
- return found;
18
- }
19
- }
20
- function JsonBlock({ value }) {
21
- return _jsx("pre", { children: pretty(value) });
22
- }
23
- function Answer({ answer, image }) {
24
- if (!answer)
25
- return _jsx("p", { className: "muted", children: "No answer received." });
26
- return (_jsxs("div", { className: "answer", children: [answer.choice !== undefined && (_jsx("strong", { className: "answer-value", children: answer.choice })), answer.score !== undefined && (_jsxs("strong", { className: "answer-value", children: [answer.score.toFixed(2), " ", _jsx("small", { children: "score" })] })), answer.noul !== undefined && (_jsxs(_Fragment, { children: [_jsxs("strong", { className: "answer-value", children: [percent(answer.noul), " ", _jsx("small", { children: "probability of true" })] }), _jsx("div", { className: "bar", children: _jsx("i", { style: { width: percent(answer.noul) } }) })] })), answer.probabilities &&
27
- Object.entries(answer.probabilities).map(([label, value]) => (_jsxs("div", { className: "probability", children: [_jsxs("div", { children: [_jsx("span", { children: label }), _jsx("b", { children: percent(value) })] }), _jsx("div", { className: "bar", children: _jsx("i", { style: { width: percent(value) } }) })] }, label))), answer.legend && (_jsxs("details", { children: [_jsx("summary", { children: "Score legend" }), _jsx(JsonBlock, { value: answer.legend })] })), answer.box && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "box-preview", children: [image ? (_jsx("img", { src: image, alt: "Captured request image" })) : (_jsx("span", { children: "Image preview unavailable" })), _jsx("div", { className: "bounding-box", style: {
28
- left: percent(answer.box.left),
29
- top: percent(answer.box.top),
30
- width: percent(answer.box.right - answer.box.left),
31
- height: percent(answer.box.bottom - answer.box.top),
32
- } })] }), _jsx("code", { className: "coordinates", children: Object.entries(answer.box)
33
- .map(([k, v]) => `${k} ${v.toFixed(2)}`)
34
- .join(" · ") })] })), answer.confidence !== undefined && (_jsxs("p", { className: "confidence", children: ["Confidence ", _jsx("b", { children: percent(answer.confidence) })] }))] }));
35
- }
36
- export function JevPanel({ traces, connection, onClear, }) {
37
- const [selected, setSelected] = useState();
15
+ // Delta updates retain unchanged Trace references. Default shallow comparison
16
+ // skips their render work while changed traces and selection still update.
17
+ const RequestRow = memo(function RequestRow({ trace, selected, onSelect, }) {
18
+ return (_jsxs("button", { className: `request-row ${selected ? "selected" : ""}`, onClick: () => onSelect(trace.id), children: [_jsx("span", { className: `status-dot ${trace.status}` }), _jsxs("span", { children: [_jsx("strong", { children: trace.label }), _jsxs("small", { children: [new Date(trace.startedAt).toLocaleTimeString(), " \u00B7 ", trace.source, " \u00B7", " ", ms(trace.durationMs)] })] }), _jsx("span", { className: "row-status", children: trace.status })] }));
19
+ });
20
+ export const RequestDetail = memo(function RequestDetail({ trace, }) {
21
+ // Keep tab selection across request switches and clear, as before. Local tab
22
+ // changes no longer rerender the history list.
38
23
  const [tab, setTab] = useState("Flow");
24
+ return (_jsx("section", { className: "request-detail", children: trace ? (_jsxs(_Fragment, { children: [_jsxs("header", { className: "detail-header", children: [_jsxs("div", { children: [_jsxs("span", { className: "eyebrow", children: [trace.source, " response \u00B7 ", trace.status] }), _jsx("h2", { children: trace.label })] }), _jsxs("div", { className: "metrics", children: [_jsxs("span", { children: [ms(trace.durationMs), _jsx("small", { children: "Total duration" })] }), _jsxs("span", { children: [trace.attempts.length, _jsx("small", { children: "Attempts" })] }), _jsxs("span", { children: [trace.result?.usage
25
+ ? `${trace.result.usage.input_tokens} / ${trace.result.usage.output_tokens}`
26
+ : "—", _jsx("small", { children: "Tokens in / out" })] })] })] }), _jsxs("div", { className: "detail-meta", children: [_jsx("code", { children: trace.result?.model || trace.request.model }), _jsx("code", { children: trace.requestId ||
27
+ (trace.status === "pending"
28
+ ? "Request ID pending"
29
+ : "No request ID") })] }), _jsx("nav", { className: "detail-tabs", "aria-label": "Request views", children: ["Flow", "Questions", "Context", "Transport", "Raw"].map((name) => (_jsx("button", { "aria-pressed": tab === name, onClick: () => {
30
+ // Keep the current view while its chunk loads, but don't
31
+ // defer subsequent opens once the component is available.
32
+ if (name === "Questions" && !questionsLoaded)
33
+ startTransition(() => setTab(name));
34
+ else
35
+ setTab(name);
36
+ }, children: name }, name))) }), _jsx("div", { className: `detail-body ${tab === "Flow" ? "detail-body-flow" : tab === "Questions" ? "detail-body-questions" : ""}`, children: _jsxs(Suspense, { fallback: _jsx("p", { className: "muted", role: "status", children: "Loading questions\u2026" }), children: [trace.error && (_jsxs("div", { role: "alert", className: "error-banner", children: [_jsx("strong", { children: trace.error.name }), " ", trace.error.message] })), trace.captureWarnings.map((w) => (_jsx("p", { className: "warning", children: w }, w))), tab === "Flow" && _jsx(TraceDiagram, { trace: trace }, trace.id), tab === "Questions" && (_jsx(Questions, { trace: trace }, trace.id)), tab === "Context" && (_jsxs(_Fragment, { children: [_jsx("p", { className: "muted", children: "Captured before dispatch. Known credential fields and email are redacted; captures are size limited." }), findImage(trace.request.state) && (_jsx("img", { className: "context-image", src: findImage(trace.request.state), alt: "Captured context" })), _jsx(JsonBlock, { value: trace.request.state }), trace.request.assets && (_jsx(JsonBlock, { value: trace.request.assets }))] })), tab === "Transport" && (_jsxs(_Fragment, { children: [_jsx("h3", { children: "HTTP attempts" }), _jsx("p", { className: "muted", children: "Attempt duration ends at response headers. Total duration includes SDK processing and retry waits." }), _jsxs("table", { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Attempt" }), _jsx("th", { children: "Status" }), _jsx("th", { children: "Duration" })] }) }), _jsx("tbody", { children: trace.attempts.map((a) => (_jsxs("tr", { children: [_jsx("td", { children: a.number }), _jsx("td", { children: a.error || a.status || "Pending" }), _jsx("td", { children: ms(a.durationMs) })] }, a.number))) })] }), !trace.attempts.length && _jsx("p", { children: "No HTTP attempt recorded." })] })), tab === "Raw" && _jsx(JsonBlock, { value: trace })] }) })] })) : (_jsxs("div", { className: "empty-detail", children: [_jsx("span", { className: "empty-symbol", children: "\u21B3" }), _jsx("h2", { children: "Follow a Jev request" }), _jsxs("p", { children: ["Context, parallel questions, typed answers.", _jsx("br", {}), "Run a sample above to see the complete flow."] })] })) }));
37
+ });
38
+ export function JevPanel({ traces, flows = emptyFlows, connection, onClear, }) {
39
+ const [view, setView] = useState("requests");
40
+ const [selectedRun, setSelectedRun] = useState();
41
+ const run = flows.runs.find((r) => r.id === selectedRun) ?? flows.runs[0];
42
+ const definition = flows.definitions.find((d) => d.id === run?.definitionId);
43
+ const [selected, setSelected] = useState();
39
44
  const [search, setSearch] = useState("");
40
45
  const [status, setStatus] = useState("all");
41
46
  const [error, setError] = useState("");
42
- const filtered = traces.filter((t) => (status === "all" || status === t.status) &&
43
- `${t.label} ${t.request.model} ${t.id}`
44
- .toLowerCase()
45
- .includes(search.toLowerCase()));
47
+ const filtered = useMemo(() => {
48
+ const query = search.toLowerCase();
49
+ return traces.filter((t) => (status === "all" || status === t.status) &&
50
+ `${t.label} ${t.request.model} ${t.id}`.toLowerCase().includes(query));
51
+ }, [traces, search, status]);
46
52
  const trace = filtered.find((t) => t.id === selected) || filtered[0];
47
- return (_jsxs("div", { className: "jev-inspector", children: [_jsxs("aside", { className: "request-list", children: [_jsxs("header", { children: [_jsxs("h2", { children: ["Jev requests ", _jsx("span", { children: traces.length })] }), _jsx("button", { onClick: () => void onClear().catch((e) => setError(e.message)), disabled: !traces.length, children: "Clear" })] }), _jsxs("div", { className: "list-filters", children: [_jsx("input", { "aria-label": "Search requests", placeholder: "Search requests\u2026", value: search, onChange: (e) => setSearch(e.target.value) }), _jsx("select", { "aria-label": "Filter request status", value: status, onChange: (e) => setStatus(e.target.value), children: ["all", "pending", "success", "error", "cancelled"].map((s) => (_jsx("option", { children: s }, s))) })] }), _jsxs("div", { className: "request-items", children: [filtered.map((t) => (_jsxs("button", { className: `request-row ${trace?.id === t.id ? "selected" : ""}`, onClick: () => setSelected(t.id), children: [_jsx("span", { className: `status-dot ${t.status}` }), _jsxs("span", { children: [_jsx("strong", { children: t.label }), _jsxs("small", { children: [new Date(t.startedAt).toLocaleTimeString(), " \u00B7 ", t.source, " \u00B7", " ", ms(t.durationMs)] })] }), _jsx("span", { className: "row-status", children: t.status })] }, t.id))), !filtered.length && (_jsx("p", { className: "empty", children: traces.length
53
+ return (_jsxs("div", { className: "jev-inspector", children: [_jsxs("aside", { className: "request-list", children: [_jsxs("header", { children: [_jsxs("h2", { children: [view === "requests" ? "Jev requests" : "Jev flows", " ", _jsx("span", { children: view === "requests" ? traces.length : flows.runs.length })] }), _jsx("button", { onClick: () => void onClear().catch((e) => setError(e.message)), disabled: !traces.length && !flows.runs.length, children: "Clear" })] }), (flows.runs.length > 0 || view === "flows") && (_jsxs("nav", { className: "detail-tabs", "aria-label": "History type", children: [_jsx("button", { "aria-pressed": view === "requests", onClick: () => setView("requests"), children: "Requests" }), _jsx("button", { "aria-pressed": view === "flows", onClick: () => {
54
+ setView("flows");
55
+ if (run)
56
+ setSelectedRun(run.id);
57
+ }, children: "Flows" })] })), view === "requests" && (_jsxs("div", { className: "list-filters", children: [_jsx("input", { "aria-label": "Search requests", placeholder: "Search requests\u2026", value: search, onChange: (e) => setSearch(e.target.value) }), _jsx("select", { "aria-label": "Filter request status", value: status, onChange: (e) => setStatus(e.target.value), children: ["all", "pending", "success", "error", "cancelled"].map((s) => (_jsx("option", { children: s }, s))) })] })), _jsxs("div", { className: "request-items", children: [view === "flows"
58
+ ? flows.runs.map((r) => (_jsxs("button", { className: `request-row ${run?.id === r.id ? "selected" : ""}`, onClick: () => setSelectedRun(r.id), children: [_jsx("span", { className: `status-dot ${r.status}` }), _jsxs("span", { children: [_jsx("strong", { children: r.name }), _jsxs("small", { children: [new Date(r.startedAt).toLocaleTimeString(), " \u00B7", " ", r.outcome ?? r.status] })] }), _jsx("span", { className: "row-status", children: r.status })] }, r.id)))
59
+ : filtered.map((t) => (_jsx(RequestRow, { trace: t, selected: trace?.id === t.id, onSelect: setSelected }, t.id))), view === "requests" && !filtered.length && (_jsx("p", { className: "empty", children: traces.length
48
60
  ? "No matching requests."
49
- : "Run a scenario to capture your first request." }))] }), (connection !== "connected" || error) && (_jsxs("footer", { children: [connection !== "connected" && (_jsx("span", { role: "status", children: connection })), error && _jsx("p", { role: "alert", children: error })] }))] }), _jsx("section", { className: "request-detail", children: trace ? (_jsxs(_Fragment, { children: [_jsxs("header", { className: "detail-header", children: [_jsxs("div", { children: [_jsxs("span", { className: "eyebrow", children: [trace.source, " response \u00B7 ", trace.status] }), _jsx("h2", { children: trace.label })] }), _jsxs("div", { className: "metrics", children: [_jsxs("span", { children: [ms(trace.durationMs), _jsx("small", { children: "Total duration" })] }), _jsxs("span", { children: [trace.attempts.length, _jsx("small", { children: "Attempts" })] }), _jsxs("span", { children: [trace.result?.usage
50
- ? `${trace.result.usage.input_tokens} / ${trace.result.usage.output_tokens}`
51
- : "—", _jsx("small", { children: "Tokens in / out" })] })] })] }), _jsxs("div", { className: "detail-meta", children: [_jsx("code", { children: trace.result?.model || trace.request.model }), _jsx("code", { children: trace.requestId ||
52
- (trace.status === "pending"
53
- ? "Request ID pending"
54
- : "No request ID") })] }), _jsx("nav", { className: "detail-tabs", "aria-label": "Request views", children: ["Flow", "Questions", "Context", "Transport", "Raw"].map((name) => (_jsx("button", { "aria-pressed": tab === name, onClick: () => setTab(name), children: name }, name))) }), _jsxs("div", { className: `detail-body ${tab === "Flow" ? "detail-body-flow" : ""}`, children: [trace.error && (_jsxs("div", { role: "alert", className: "error-banner", children: [_jsx("strong", { children: trace.error.name }), " ", trace.error.message] })), trace.captureWarnings.map((w) => (_jsx("p", { className: "warning", children: w }, w))), tab === "Flow" && _jsx(TraceDiagram, { trace: trace }, trace.id), tab === "Questions" &&
55
- Object.entries(trace.request.questions ?? {})
56
- .filter(([, q]) => q && typeof q === "object" && typeof q.type === "string")
57
- .map(([key, q]) => (_jsxs("article", { className: "question-node standalone", children: [_jsxs("header", { children: [_jsx("h3", { children: key }), _jsx("span", { className: "type", children: q.type })] }), _jsx(JsonBlock, { value: q }), _jsx(Answer, { answer: trace.result?.answers[key], image: findImage(trace.request.state) })] }, key))), tab === "Context" && (_jsxs(_Fragment, { children: [_jsx("p", { className: "muted", children: "Captured before dispatch. Known credential fields and email are redacted; captures are size limited." }), findImage(trace.request.state) && (_jsx("img", { className: "context-image", src: findImage(trace.request.state), alt: "Captured context" })), _jsx(JsonBlock, { value: trace.request.state }), trace.request.assets && (_jsx(JsonBlock, { value: trace.request.assets }))] })), tab === "Transport" && (_jsxs(_Fragment, { children: [_jsx("h3", { children: "HTTP attempts" }), _jsx("p", { className: "muted", children: "Attempt duration ends at response headers. Total duration includes SDK processing and retry waits." }), _jsxs("table", { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Attempt" }), _jsx("th", { children: "Status" }), _jsx("th", { children: "Duration" })] }) }), _jsx("tbody", { children: trace.attempts.map((a) => (_jsxs("tr", { children: [_jsx("td", { children: a.number }), _jsx("td", { children: a.error || a.status || "Pending" }), _jsx("td", { children: ms(a.durationMs) })] }, a.number))) })] }), !trace.attempts.length && _jsx("p", { children: "No HTTP attempt recorded." })] })), tab === "Raw" && _jsx(JsonBlock, { value: trace })] })] })) : (_jsxs("div", { className: "empty-detail", children: [_jsx("span", { className: "empty-symbol", children: "\u21B3" }), _jsx("h2", { children: "Follow a Jev request" }), _jsxs("p", { children: ["Context, parallel questions, typed answers.", _jsx("br", {}), "Run a sample above to see the complete flow."] })] })) })] }));
61
+ : "Run a scenario to capture your first request." }))] }), (connection !== "connected" || error) && (_jsxs("footer", { children: [connection !== "connected" && (_jsx("span", { role: "status", children: connection })), error && _jsx("p", { role: "alert", children: error })] }))] }), view === "flows" && run && definition ? (_jsx(Suspense, { fallback: _jsx("p", { role: "status", children: "Loading flow\u2026" }), children: _jsx(FlowOverview, { run: run, definition: definition, traces: traces }, run.id) })) : (_jsx(RequestDetail, { trace: trace }))] }));
58
62
  }
@@ -1,6 +1,7 @@
1
1
  import { type ReactNode } from "react";
2
2
  /** Non-modal drawer: the app remains usable and children stay mounted when closed. */
3
- export declare function JevDrawer({ children, defaultOpen, }: {
3
+ export declare function JevDrawer({ children, open, onOpenChange, }: {
4
4
  children: ReactNode;
5
- defaultOpen?: boolean;
5
+ open: boolean;
6
+ onOpenChange: (open: boolean) => void;
6
7
  }): import("react").ReactPortal | null;
@@ -25,10 +25,9 @@ function initialHeight() {
25
25
  return clamp(Math.min(420, viewportHeight() * 0.56), viewportHeight());
26
26
  }
27
27
  /** Non-modal drawer: the app remains usable and children stay mounted when closed. */
28
- export function JevDrawer({ children, defaultOpen = true, }) {
28
+ export function JevDrawer({ children, open, onOpenChange, }) {
29
29
  const id = useId();
30
30
  const [root, setRoot] = useState(null);
31
- const [open, setOpen] = useState(defaultOpen);
32
31
  const [expanded, setExpanded] = useState(false);
33
32
  const [viewport, setViewport] = useState(viewportHeight);
34
33
  const [height, setHeight] = useState(initialHeight);
@@ -39,6 +38,16 @@ export function JevDrawer({ children, defaultOpen = true, }) {
39
38
  const drag = useRef(null);
40
39
  const { min, max } = limits(viewport);
41
40
  const visibleHeight = expanded ? max : clamp(height, viewport);
41
+ useEffect(() => {
42
+ // Retain the diagram and its camera, but suspend native SVG timelines while
43
+ // hidden. This does not change the user's Pause/Play flow preference.
44
+ panel.current?.querySelectorAll("svg").forEach((svg) => {
45
+ if (open)
46
+ svg.unpauseAnimations();
47
+ else
48
+ svg.pauseAnimations();
49
+ });
50
+ }, [open, root]);
42
51
  useEffect(() => {
43
52
  const host = document.createElement("jev-devtools-root");
44
53
  // The host is the only element exposed to page selectors. The overlay itself
@@ -76,7 +85,7 @@ export function JevDrawer({ children, defaultOpen = true, }) {
76
85
  }, [open, root]);
77
86
  const toggle = (value) => {
78
87
  interacted.current = true;
79
- setOpen(value);
88
+ onOpenChange(value);
80
89
  };
81
90
  const resizeTo = (value) => {
82
91
  setExpanded(false);
@@ -0,0 +1,5 @@
1
+ import type { Trace } from "./protocol.js";
2
+ /** Variable-height rows retain full answers; only offscreen DOM is omitted. */
3
+ export declare function Questions({ trace }: {
4
+ trace: Trace;
5
+ }): import("react").JSX.Element;