causal-inspector 0.1.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 (68) hide show
  1. package/dist/CausalInspector.css +124 -0
  2. package/dist/CausalInspector.d.ts +13 -0
  3. package/dist/CausalInspector.js +257 -0
  4. package/dist/components/CopyablePayload.d.ts +4 -0
  5. package/dist/components/CopyablePayload.js +44 -0
  6. package/dist/components/FilterBar.d.ts +1 -0
  7. package/dist/components/FilterBar.js +23 -0
  8. package/dist/components/GlobalScrubber.d.ts +1 -0
  9. package/dist/components/GlobalScrubber.js +148 -0
  10. package/dist/components/JsonSyntax.d.ts +3 -0
  11. package/dist/components/JsonSyntax.js +40 -0
  12. package/dist/context.d.ts +26 -0
  13. package/dist/context.js +28 -0
  14. package/dist/engines/index.d.ts +22 -0
  15. package/dist/engines/index.js +29 -0
  16. package/dist/engines/query.d.ts +14 -0
  17. package/dist/engines/query.js +241 -0
  18. package/dist/engines/scrubber.d.ts +12 -0
  19. package/dist/engines/scrubber.js +69 -0
  20. package/dist/engines/storage.d.ts +15 -0
  21. package/dist/engines/storage.js +16 -0
  22. package/dist/engines/subscription.d.ts +17 -0
  23. package/dist/engines/subscription.js +44 -0
  24. package/dist/engines/url.d.ts +13 -0
  25. package/dist/engines/url.js +64 -0
  26. package/dist/events.d.ts +77 -0
  27. package/dist/events.js +1 -0
  28. package/dist/index.d.ts +30 -0
  29. package/dist/index.js +29 -0
  30. package/dist/machine/core.d.ts +22 -0
  31. package/dist/machine/core.js +34 -0
  32. package/dist/machine/engine.d.ts +17 -0
  33. package/dist/machine/engine.js +21 -0
  34. package/dist/machine/events.d.ts +18 -0
  35. package/dist/machine/events.js +1 -0
  36. package/dist/machine/hooks.d.ts +23 -0
  37. package/dist/machine/hooks.js +52 -0
  38. package/dist/machine/index.d.ts +5 -0
  39. package/dist/machine/index.js +4 -0
  40. package/dist/machine/store.d.ts +27 -0
  41. package/dist/machine/store.js +42 -0
  42. package/dist/panes/AggregateTimelinePane.d.ts +2 -0
  43. package/dist/panes/AggregateTimelinePane.js +224 -0
  44. package/dist/panes/CausalFlowPane.d.ts +7 -0
  45. package/dist/panes/CausalFlowPane.js +596 -0
  46. package/dist/panes/CausalTreePane.d.ts +5 -0
  47. package/dist/panes/CausalTreePane.js +158 -0
  48. package/dist/panes/CorrelationExplorerPane.d.ts +2 -0
  49. package/dist/panes/CorrelationExplorerPane.js +46 -0
  50. package/dist/panes/LogsPane.d.ts +6 -0
  51. package/dist/panes/LogsPane.js +65 -0
  52. package/dist/panes/TimelinePane.d.ts +6 -0
  53. package/dist/panes/TimelinePane.js +121 -0
  54. package/dist/panes/WaterfallPane.d.ts +2 -0
  55. package/dist/panes/WaterfallPane.js +202 -0
  56. package/dist/queries.d.ts +15 -0
  57. package/dist/queries.js +175 -0
  58. package/dist/reducer.d.ts +4 -0
  59. package/dist/reducer.js +177 -0
  60. package/dist/state.d.ts +34 -0
  61. package/dist/state.js +39 -0
  62. package/dist/theme.d.ts +7 -0
  63. package/dist/theme.js +34 -0
  64. package/dist/types.d.ts +140 -0
  65. package/dist/types.js +1 -0
  66. package/dist/utils.d.ts +14 -0
  67. package/dist/utils.js +91 -0
  68. package/package.json +43 -0
@@ -0,0 +1,596 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useCallback, useEffect, useRef, useState, memo } from "react";
3
+ import { ReactFlow, Background, Controls, useReactFlow, Handle, MarkerType, Position, } from "@xyflow/react";
4
+ import dagre from "@dagrejs/dagre";
5
+ import { useSelector, useDispatch } from "../machine";
6
+ import { eventBg, eventBorder, eventTextColor } from "../theme";
7
+ import { Filter } from "lucide-react";
8
+ import { inScrubberRange } from "../utils";
9
+ /* eslint-disable-next-line @typescript-eslint/no-redeclare -- shadowing the tree-pane ReactorNode on purpose */
10
+ const NODE_WIDTH = 180;
11
+ const NODE_HEIGHT = 36;
12
+ const REACTOR_WIDTH = 180;
13
+ const REACTOR_HEIGHT = 36;
14
+ // ---------------------------------------------------------------------------
15
+ // Block renderers
16
+ // ---------------------------------------------------------------------------
17
+ function BlockRenderer({ block }) {
18
+ switch (block.type) {
19
+ case "checklist":
20
+ return (_jsxs("div", { style: { marginTop: 4 }, children: [_jsx("div", { style: { fontSize: 9, color: "#71717a", marginBottom: 2 }, children: block.label }), block.items.map((item, i) => (_jsxs("div", { style: { fontSize: 9, color: item.done ? "#22c55e" : "#52525b", display: "flex", gap: 3, alignItems: "center" }, children: [_jsx("span", { children: item.done ? "\u2713" : "\u25cb" }), _jsx("span", { children: item.text })] }, i)))] }));
21
+ case "counter":
22
+ return (_jsxs("div", { style: { fontSize: 9, color: "#a1a1aa", marginTop: 2 }, children: [block.label, ": ", block.value, "/", block.total] }));
23
+ case "progress": {
24
+ const pct = Math.round(block.fraction * 100);
25
+ return (_jsxs("div", { style: { marginTop: 2 }, children: [_jsxs("div", { style: { fontSize: 9, color: "#a1a1aa" }, children: [block.label, ": ", pct, "%"] }), _jsx("div", { style: { height: 3, background: "#3f3f46", borderRadius: 2, marginTop: 1 }, children: _jsx("div", { style: { height: "100%", width: `${pct}%`, background: "#22c55e", borderRadius: 2 } }) })] }));
26
+ }
27
+ case "label":
28
+ return _jsx("div", { style: { fontSize: 9, color: "#a1a1aa", marginTop: 2 }, children: block.text });
29
+ case "key_value":
30
+ return (_jsxs("div", { style: { fontSize: 9, color: "#a1a1aa", marginTop: 2 }, children: [_jsxs("span", { style: { color: "#71717a" }, children: [block.key, ":"] }), " ", block.value] }));
31
+ case "status": {
32
+ const colors = { waiting: "#71717a", running: "#eab308", done: "#22c55e", error: "#ef4444" };
33
+ return (_jsxs("div", { style: { fontSize: 9, color: colors[block.state] ?? "#a1a1aa", marginTop: 2 }, children: [block.label, ": ", block.state] }));
34
+ }
35
+ default:
36
+ return null;
37
+ }
38
+ }
39
+ // ---------------------------------------------------------------------------
40
+ // Custom nodes
41
+ // ---------------------------------------------------------------------------
42
+ function formatDuration(startedAt, completedAt) {
43
+ const ms = new Date(completedAt).getTime() - new Date(startedAt).getTime();
44
+ if (ms < 1000)
45
+ return `${ms}ms`;
46
+ const secs = ms / 1000;
47
+ if (secs < 60)
48
+ return `${secs.toFixed(1)}s`;
49
+ const mins = Math.floor(secs / 60);
50
+ const remainSecs = Math.round(secs % 60);
51
+ return remainSecs > 0 ? `${mins}m ${remainSecs}s` : `${mins}m`;
52
+ }
53
+ const STATUS_BORDER = {
54
+ pending: "#52525b",
55
+ running: "#eab308",
56
+ completed: "#22c55e",
57
+ error: "#ef4444",
58
+ };
59
+ const ReactorNode = memo(({ data }) => {
60
+ const d = data;
61
+ const blocks = d.blocks;
62
+ const outcome = d.outcome;
63
+ const hasBlocks = blocks && blocks.length > 0;
64
+ const borderColor = STATUS_BORDER[outcome?.status ?? "pending"] ?? "#2a2a35";
65
+ const isRunning = outcome?.status === "running";
66
+ const duration = outcome?.status === "completed" && outcome.startedAt && outcome.completedAt
67
+ ? formatDuration(outcome.startedAt, outcome.completedAt)
68
+ : null;
69
+ return (_jsxs("div", { style: {
70
+ background: "linear-gradient(135deg, #1a1a22, #15151d)",
71
+ border: `1px solid ${borderColor}`,
72
+ borderRadius: hasBlocks ? 10 : 20,
73
+ fontSize: 10,
74
+ padding: hasBlocks ? "8px 12px" : "6px 14px",
75
+ width: REACTOR_WIDTH,
76
+ color: "#9090a0",
77
+ fontStyle: "italic",
78
+ animation: isRunning ? "pulse 2s ease-in-out infinite" : undefined,
79
+ boxShadow: isRunning
80
+ ? `0 0 12px ${borderColor}40`
81
+ : "0 2px 8px rgba(0, 0, 0, 0.3)",
82
+ }, children: [_jsx(Handle, { type: "target", position: Position.Left, style: { visibility: "hidden" } }), _jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8 }, children: [_jsx("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", direction: "rtl", textAlign: "left" }, children: d.label }), duration && _jsx("span", { style: { fontSize: 9, color: "#60608a", fontStyle: "normal", whiteSpace: "nowrap", flexShrink: 0 }, children: duration })] }), hasBlocks && blocks.map((block, i) => _jsx(BlockRenderer, { block: block }, i)), outcome?.status === "error" && outcome.error && (_jsx("div", { style: { fontSize: 9, color: "#ef4444", marginTop: 4 }, children: outcome.error })), _jsx(Handle, { type: "source", position: Position.Right, style: { visibility: "hidden" } })] }));
83
+ });
84
+ const EventNode = memo(({ data }) => {
85
+ const d = data;
86
+ return (_jsxs("div", { style: {
87
+ background: eventBg(d.eventName),
88
+ border: `1px solid ${eventBorder(d.eventName)}`,
89
+ borderRadius: 8,
90
+ fontSize: 11,
91
+ padding: "7px 12px",
92
+ width: NODE_WIDTH,
93
+ color: eventTextColor(d.eventName),
94
+ overflow: "hidden",
95
+ textOverflow: "ellipsis",
96
+ whiteSpace: "nowrap",
97
+ direction: "rtl",
98
+ textAlign: "left",
99
+ boxShadow: `0 2px 8px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.04)`,
100
+ fontWeight: 500,
101
+ letterSpacing: "0.01em",
102
+ }, children: [_jsx(Handle, { type: "target", position: Position.Left, style: { visibility: "hidden" } }), d.label, _jsx(Handle, { type: "source", position: Position.Right, style: { visibility: "hidden" } })] }));
103
+ });
104
+ const nodeTypes = { reactor: ReactorNode, event: EventNode };
105
+ function buildFlowGraph(events, descriptions, outcomes, hiddenReactors) {
106
+ // Group events by type name only (merge across reactors)
107
+ const eventGroups = new Map();
108
+ const reactorIds = new Set();
109
+ const parentToReactor = new Map();
110
+ const reactorToChildTypes = new Map();
111
+ for (const evt of events) {
112
+ const groupKey = evt.name;
113
+ const group = eventGroups.get(groupKey);
114
+ if (group) {
115
+ group.count++;
116
+ group.events.push(evt);
117
+ }
118
+ else {
119
+ eventGroups.set(groupKey, { name: evt.name, count: 1, events: [evt] });
120
+ }
121
+ if (evt.reactorId) {
122
+ reactorIds.add(evt.reactorId);
123
+ const children = reactorToChildTypes.get(evt.reactorId) ?? new Set();
124
+ children.add(groupKey);
125
+ reactorToChildTypes.set(evt.reactorId, children);
126
+ }
127
+ if (evt.parentId && evt.reactorId) {
128
+ const reactors = parentToReactor.get(evt.parentId) ?? new Set();
129
+ reactors.add(evt.reactorId);
130
+ parentToReactor.set(evt.parentId, reactors);
131
+ }
132
+ }
133
+ const eventIdToGroup = new Map();
134
+ for (const [groupKey, group] of eventGroups) {
135
+ for (const evt of group.events) {
136
+ if (evt.id)
137
+ eventIdToGroup.set(evt.id, groupKey);
138
+ }
139
+ }
140
+ const nodes = [];
141
+ const edges = [];
142
+ const edgeSet = new Set();
143
+ // Event-type nodes (one per event name)
144
+ for (const [groupKey, group] of eventGroups) {
145
+ // Skip if ALL emitting reactors are hidden
146
+ const emittingReactors = new Set(group.events.map((e) => e.reactorId).filter(Boolean));
147
+ if (emittingReactors.size > 0 && hiddenReactors && [...emittingReactors].every((r) => hiddenReactors.has(r)))
148
+ continue;
149
+ nodes.push({
150
+ id: `evt:${groupKey}`,
151
+ type: "event",
152
+ position: { x: 0, y: 0 },
153
+ data: {
154
+ label: group.count > 1 ? `${group.name} (${group.count})` : group.name,
155
+ nodeKind: "event-type",
156
+ eventName: group.name,
157
+ },
158
+ sourcePosition: Position.Right,
159
+ targetPosition: Position.Left,
160
+ });
161
+ }
162
+ // Reactor nodes
163
+ for (const reactorId of reactorIds) {
164
+ if (hiddenReactors?.has(reactorId))
165
+ continue;
166
+ const blocks = descriptions?.get(reactorId);
167
+ const outcome = outcomes?.get(reactorId);
168
+ nodes.push({
169
+ id: `hdl:${reactorId}`,
170
+ type: "reactor",
171
+ position: { x: 0, y: 0 },
172
+ data: { label: reactorId, nodeKind: "reactor", reactorId, blocks, outcome },
173
+ sourcePosition: Position.Right,
174
+ targetPosition: Position.Left,
175
+ });
176
+ }
177
+ const arrowMarker = { type: MarkerType.ArrowClosed, color: "#3a3a4a", width: 14, height: 14 };
178
+ // Edges: event type -> reactor (event triggers reactor)
179
+ for (const [parentId, reactors] of parentToReactor) {
180
+ const sourceGroupKey = eventIdToGroup.get(parentId);
181
+ if (!sourceGroupKey)
182
+ continue;
183
+ for (const reactorId of reactors) {
184
+ if (hiddenReactors?.has(reactorId))
185
+ continue;
186
+ const edgeKey = `evt:${sourceGroupKey}->hdl:${reactorId}`;
187
+ if (!edgeSet.has(edgeKey)) {
188
+ edgeSet.add(edgeKey);
189
+ edges.push({
190
+ id: edgeKey,
191
+ source: `evt:${sourceGroupKey}`,
192
+ target: `hdl:${reactorId}`,
193
+ style: { stroke: "#3a3a4a", strokeWidth: 1 },
194
+ markerEnd: arrowMarker,
195
+ });
196
+ }
197
+ }
198
+ }
199
+ // Edges: reactor -> child event types (reactor produces events)
200
+ for (const [reactorId, childTypes] of reactorToChildTypes) {
201
+ if (hiddenReactors?.has(reactorId))
202
+ continue;
203
+ for (const typeName of childTypes) {
204
+ const edgeKey = `hdl:${reactorId}->evt:${typeName}`;
205
+ if (!edgeSet.has(edgeKey)) {
206
+ edgeSet.add(edgeKey);
207
+ edges.push({
208
+ id: edgeKey,
209
+ source: `hdl:${reactorId}`,
210
+ target: `evt:${typeName}`,
211
+ style: { stroke: "#3a3a4a", strokeWidth: 1 },
212
+ markerEnd: arrowMarker,
213
+ });
214
+ }
215
+ }
216
+ }
217
+ // Reactors known from outcomes but not from event stream
218
+ if (outcomes) {
219
+ for (const [reactorId, outcome] of outcomes) {
220
+ if (reactorIds.has(reactorId))
221
+ continue;
222
+ if (hiddenReactors?.has(reactorId))
223
+ continue;
224
+ const blocks = descriptions?.get(reactorId);
225
+ nodes.push({
226
+ id: `hdl:${reactorId}`,
227
+ type: "reactor",
228
+ position: { x: 0, y: 0 },
229
+ data: { label: reactorId, nodeKind: "reactor", reactorId, blocks, outcome },
230
+ sourcePosition: Position.Bottom,
231
+ targetPosition: Position.Top,
232
+ });
233
+ const isPending = outcome.status === "pending" || outcome.status === "running";
234
+ for (const eventId of outcome.triggeringEventIds ?? []) {
235
+ const groupKey = eventIdToGroup.get(eventId);
236
+ if (!groupKey)
237
+ continue;
238
+ const edgeKey = `evt:${groupKey}->hdl:${reactorId}`;
239
+ if (!edgeSet.has(edgeKey)) {
240
+ edgeSet.add(edgeKey);
241
+ edges.push({
242
+ id: edgeKey,
243
+ source: `evt:${groupKey}`,
244
+ target: `hdl:${reactorId}`,
245
+ style: { stroke: "#3a3a4a", strokeWidth: 1 },
246
+ markerEnd: arrowMarker,
247
+ animated: isPending,
248
+ });
249
+ }
250
+ }
251
+ }
252
+ }
253
+ return layoutGraph(nodes, edges);
254
+ }
255
+ function estimateReactorHeight(data) {
256
+ if (data.nodeKind !== "reactor")
257
+ return REACTOR_HEIGHT;
258
+ const hasBlocks = data.blocks && data.blocks.length > 0;
259
+ const outcome = data.outcome;
260
+ if (!hasBlocks && !outcome)
261
+ return REACTOR_HEIGHT;
262
+ let h = 24;
263
+ if (data.blocks) {
264
+ for (const block of data.blocks) {
265
+ if (block.type === "checklist") {
266
+ h += 14 + block.items.length * 12;
267
+ }
268
+ else {
269
+ h += 14;
270
+ }
271
+ }
272
+ }
273
+ if (outcome?.status === "error" && outcome.error)
274
+ h += 14;
275
+ return h;
276
+ }
277
+ function layoutGraph(nodes, edges) {
278
+ const g = new dagre.graphlib.Graph();
279
+ g.setDefaultEdgeLabel(() => ({}));
280
+ g.setGraph({ rankdir: "LR", nodesep: 40, ranksep: 80 });
281
+ const heights = new Map();
282
+ for (const node of nodes) {
283
+ const isReactor = node.id.startsWith("hdl:");
284
+ const h = isReactor ? estimateReactorHeight(node.data) : NODE_HEIGHT;
285
+ heights.set(node.id, h);
286
+ g.setNode(node.id, {
287
+ width: isReactor ? REACTOR_WIDTH : NODE_WIDTH,
288
+ height: h,
289
+ });
290
+ }
291
+ for (const edge of edges) {
292
+ g.setEdge(edge.source, edge.target);
293
+ }
294
+ dagre.layout(g);
295
+ const laidOut = nodes.map((node) => {
296
+ const pos = g.node(node.id);
297
+ const isReactor = node.id.startsWith("hdl:");
298
+ const w = isReactor ? REACTOR_WIDTH : NODE_WIDTH;
299
+ const h = heights.get(node.id) ?? NODE_HEIGHT;
300
+ return {
301
+ ...node,
302
+ position: { x: pos.x - w / 2, y: pos.y - h / 2 },
303
+ };
304
+ });
305
+ return { nodes: laidOut, edges };
306
+ }
307
+ // ---------------------------------------------------------------------------
308
+ // Scrubber visibility — compute which nodes/edges are visible at a given seq
309
+ // ---------------------------------------------------------------------------
310
+ function computeVisibleIds(allEvents, start, end) {
311
+ const visible = allEvents.filter((e) => inScrubberRange(e.seq, start, end));
312
+ const nodeIds = new Set();
313
+ const edgeIds = new Set();
314
+ const eventIdToGroup = new Map();
315
+ const seenTypes = new Set();
316
+ for (const evt of visible) {
317
+ const groupKey = evt.name;
318
+ seenTypes.add(groupKey);
319
+ if (evt.id)
320
+ eventIdToGroup.set(evt.id, groupKey);
321
+ }
322
+ // Event-type nodes
323
+ for (const typeName of seenTypes) {
324
+ nodeIds.add(`evt:${typeName}`);
325
+ }
326
+ // Reactor nodes + edges
327
+ for (const evt of visible) {
328
+ if (evt.reactorId) {
329
+ nodeIds.add(`hdl:${evt.reactorId}`);
330
+ // Reactor -> child event type edge
331
+ edgeIds.add(`hdl:${evt.reactorId}->evt:${evt.name}`);
332
+ }
333
+ // Parent event -> reactor edge
334
+ if (evt.parentId && evt.reactorId) {
335
+ const parentGroup = eventIdToGroup.get(evt.parentId);
336
+ if (parentGroup) {
337
+ edgeIds.add(`evt:${parentGroup}->hdl:${evt.reactorId}`);
338
+ }
339
+ }
340
+ // Root event -> reactor edges
341
+ if (!evt.reactorId && evt.id) {
342
+ for (const child of visible) {
343
+ if (child.parentId === evt.id && child.reactorId) {
344
+ const rootGroup = eventIdToGroup.get(evt.id);
345
+ if (rootGroup) {
346
+ edgeIds.add(`evt:${rootGroup}->hdl:${child.reactorId}`);
347
+ }
348
+ }
349
+ }
350
+ }
351
+ }
352
+ return { nodeIds, edgeIds };
353
+ }
354
+ // ---------------------------------------------------------------------------
355
+ // Auto-center on selection
356
+ // ---------------------------------------------------------------------------
357
+ function FitOnLoad() {
358
+ const { fitView } = useReactFlow();
359
+ const fitted = useRef(false);
360
+ const flowData = useSelector((s) => s.flowData);
361
+ useEffect(() => {
362
+ if (!fitted.current && flowData.length > 0) {
363
+ fitted.current = true;
364
+ // Delay slightly to let ReactFlow measure nodes
365
+ requestAnimationFrame(() => fitView({ duration: 300 }));
366
+ }
367
+ }, [flowData, fitView]);
368
+ return null;
369
+ }
370
+ function FocusOnSelection({ nodes, flowData }) {
371
+ const selectedSeq = useSelector((s) => s.selectedSeq);
372
+ const scrubberEnd = useSelector((s) => s.scrubberEnd);
373
+ const { setCenter, getZoom } = useReactFlow();
374
+ const nodesRef = useRef(nodes);
375
+ nodesRef.current = nodes;
376
+ useEffect(() => {
377
+ // Don't recenter while scrubber is active
378
+ if (scrubberEnd != null)
379
+ return;
380
+ if (selectedSeq == null || !flowData.length)
381
+ return;
382
+ const evt = flowData.find(e => e.seq === selectedSeq);
383
+ if (!evt)
384
+ return;
385
+ const nodeId = `evt:${evt.name}`;
386
+ const node = nodesRef.current.find(n => n.id === nodeId);
387
+ if (!node)
388
+ return;
389
+ const isReactor = node.id.startsWith("hdl:");
390
+ const w = isReactor ? REACTOR_WIDTH : NODE_WIDTH;
391
+ const h = isReactor ? estimateReactorHeight(node.data) : NODE_HEIGHT;
392
+ setCenter(node.position.x + w / 2, node.position.y + h / 2, { zoom: getZoom(), duration: 400 });
393
+ }, [selectedSeq, scrubberEnd, flowData, setCenter, getZoom]);
394
+ return null;
395
+ }
396
+ // ---------------------------------------------------------------------------
397
+ // Reactor filter dropdown
398
+ // ---------------------------------------------------------------------------
399
+ function ReactorFilter({ allReactorIds, hiddenReactors, setHiddenReactors }) {
400
+ const [open, setOpen] = useState(false);
401
+ const [filter, setFilter] = useState("");
402
+ const containerRef = useRef(null);
403
+ // Close on click outside
404
+ useEffect(() => {
405
+ if (!open)
406
+ return;
407
+ const handleClick = (e) => {
408
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
409
+ setOpen(false);
410
+ }
411
+ };
412
+ document.addEventListener("mousedown", handleClick);
413
+ return () => document.removeEventListener("mousedown", handleClick);
414
+ }, [open]);
415
+ const toggle = (id) => {
416
+ const next = new Set(hiddenReactors);
417
+ if (next.has(id))
418
+ next.delete(id);
419
+ else
420
+ next.add(id);
421
+ setHiddenReactors(next);
422
+ };
423
+ const filtered = filter
424
+ ? allReactorIds.filter(id => id.toLowerCase().includes(filter.toLowerCase()))
425
+ : allReactorIds;
426
+ const hiddenCount = hiddenReactors.size;
427
+ return (_jsxs("div", { ref: containerRef, className: "relative", children: [_jsxs("button", { onClick: () => setOpen(v => !v), className: "text-[10px] text-muted-foreground/60 hover:text-foreground px-2 py-1 rounded-md border border-border hover:border-indigo-500/30 transition-all duration-150", children: [_jsx(Filter, { size: 11, className: "inline mr-1 -mt-px" }), hiddenCount > 0 ? `${hiddenCount} hidden` : "Filter"] }), open && (_jsxs("div", { className: "absolute top-full right-0 mt-1 z-50 border border-border rounded-lg min-w-[240px]", style: { background: "rgba(17, 17, 22, 0.95)", backdropFilter: "blur(12px)", boxShadow: "0 8px 32px rgba(0, 0, 0, 0.5)" }, children: [_jsx("div", { className: "px-3 py-2 border-b border-border", children: _jsx("input", { autoFocus: true, type: "text", value: filter, onChange: e => setFilter(e.target.value), placeholder: "Search reactors...", className: "w-full text-xs bg-transparent border-none outline-none text-foreground placeholder:text-muted-foreground/50" }) }), _jsxs("div", { className: "max-h-64 overflow-y-auto py-1", children: [filtered.map(id => (_jsxs("label", { className: "flex items-center gap-2 px-3 py-1.5 hover:bg-white/[0.03] cursor-pointer transition-colors", children: [_jsx("input", { type: "checkbox", checked: !hiddenReactors.has(id), onChange: () => toggle(id), className: "rounded border-border accent-indigo-500" }), _jsx("span", { className: "text-[11px] font-mono text-foreground/80 truncate", children: id })] }, id))), filtered.length === 0 && (_jsx("div", { className: "text-xs text-muted-foreground/50 px-3 py-2", children: "No matches" }))] })] }))] }));
428
+ }
429
+ export function CausalFlowPane({ defaultHiddenReactors, headerExtra } = {}) {
430
+ const flowCorrelationId = useSelector((s) => s.flowCorrelationId);
431
+ const flowData = useSelector((s) => s.flowData);
432
+ const flowSelection = useSelector((s) => s.flowSelection);
433
+ const descriptionsMap = useSelector((s) => s.descriptions);
434
+ const outcomesMap = useSelector((s) => s.outcomes);
435
+ const scrubberStart = useSelector((s) => s.scrubberStart);
436
+ const scrubberEnd = useSelector((s) => s.scrubberEnd);
437
+ const dispatch = useDispatch();
438
+ const flowLoading = flowCorrelationId != null && flowData.length === 0;
439
+ // Build typed maps from state
440
+ const descriptions = useMemo(() => {
441
+ if (!flowCorrelationId)
442
+ return undefined;
443
+ const raw = descriptionsMap[flowCorrelationId];
444
+ if (!raw)
445
+ return undefined;
446
+ const map = new Map();
447
+ for (const d of raw)
448
+ map.set(d.reactorId, d.blocks);
449
+ return map;
450
+ }, [descriptionsMap, flowCorrelationId]);
451
+ const outcomes = useMemo(() => {
452
+ if (!flowCorrelationId)
453
+ return undefined;
454
+ const raw = outcomesMap[flowCorrelationId];
455
+ if (!raw)
456
+ return undefined;
457
+ const map = new Map();
458
+ for (const o of raw)
459
+ map.set(o.reactorId, o);
460
+ return map;
461
+ }, [outcomesMap, flowCorrelationId]);
462
+ const [hiddenReactors, setHiddenReactors] = useState(() => defaultHiddenReactors ?? new Set());
463
+ const allReactorIds = useMemo(() => {
464
+ const ids = new Set();
465
+ for (const evt of flowData) {
466
+ if (evt.reactorId)
467
+ ids.add(evt.reactorId);
468
+ }
469
+ if (outcomes)
470
+ for (const id of outcomes.keys())
471
+ ids.add(id);
472
+ return [...ids].sort();
473
+ }, [flowData, outcomes]);
474
+ // Full graph layout — stable positions computed from ALL events
475
+ const { nodes: fullNodes, edges: fullEdges } = useMemo(() => {
476
+ if (!flowData || flowData.length === 0)
477
+ return { nodes: [], edges: [] };
478
+ return buildFlowGraph(flowData, descriptions, outcomes, hiddenReactors);
479
+ }, [flowData, descriptions, outcomes, hiddenReactors]);
480
+ // Compute visible IDs when scrubber range is active
481
+ const visibleIds = useMemo(() => {
482
+ if (scrubberStart == null && scrubberEnd == null)
483
+ return null; // show everything
484
+ return computeVisibleIds(flowData, scrubberStart, scrubberEnd);
485
+ }, [flowData, scrubberStart, scrubberEnd]);
486
+ // Apply visibility: hidden nodes get opacity 0, hidden edges are filtered out
487
+ const rawNodes = useMemo(() => {
488
+ if (!visibleIds)
489
+ return fullNodes;
490
+ return fullNodes.map((n) => ({
491
+ ...n,
492
+ hidden: !visibleIds.nodeIds.has(n.id),
493
+ }));
494
+ }, [fullNodes, visibleIds]);
495
+ const rawEdges = useMemo(() => {
496
+ if (!visibleIds)
497
+ return fullEdges;
498
+ return fullEdges.map((e) => ({
499
+ ...e,
500
+ hidden: !visibleIds.edgeIds.has(e.id),
501
+ }));
502
+ }, [fullEdges, visibleIds]);
503
+ // Derive selected node ID from flowSelection
504
+ const selectedNodeId = useMemo(() => {
505
+ if (!flowSelection)
506
+ return null;
507
+ if (flowSelection.kind === "reactor")
508
+ return `hdl:${flowSelection.reactorId}`;
509
+ return `evt:${flowSelection.name}`;
510
+ }, [flowSelection]);
511
+ // Walk causal chain for highlighting
512
+ const causalNodeIds = useMemo(() => {
513
+ if (!selectedNodeId)
514
+ return null;
515
+ const forward = new Map();
516
+ const backward = new Map();
517
+ for (const e of rawEdges) {
518
+ forward.set(e.source, [...(forward.get(e.source) ?? []), e.target]);
519
+ backward.set(e.target, [...(backward.get(e.target) ?? []), e.source]);
520
+ }
521
+ const visited = new Set();
522
+ const walk = (id, adj) => {
523
+ if (visited.has(id))
524
+ return;
525
+ visited.add(id);
526
+ for (const next of adj.get(id) ?? [])
527
+ walk(next, adj);
528
+ };
529
+ walk(selectedNodeId, forward);
530
+ walk(selectedNodeId, backward);
531
+ return visited;
532
+ }, [selectedNodeId, rawEdges]);
533
+ const nodes = useMemo(() => rawNodes.map(n => ({
534
+ ...n,
535
+ selected: n.id === selectedNodeId,
536
+ style: {
537
+ ...n.style,
538
+ ...(causalNodeIds != null && !causalNodeIds.has(n.id) ? { opacity: 0.5 } : {}),
539
+ },
540
+ })), [rawNodes, selectedNodeId, causalNodeIds]);
541
+ const edges = useMemo(() => rawEdges.map(e => {
542
+ const base = { ...e, zIndex: -1 };
543
+ if (!causalNodeIds)
544
+ return base;
545
+ const onPath = causalNodeIds.has(e.source) && causalNodeIds.has(e.target);
546
+ return {
547
+ ...base,
548
+ style: {
549
+ ...e.style,
550
+ stroke: onPath ? "#818cf8" : "#3a3a4a",
551
+ strokeWidth: onPath ? 2 : 1,
552
+ opacity: onPath ? 1 : 0.15,
553
+ },
554
+ markerEnd: onPath
555
+ ? { type: MarkerType.ArrowClosed, color: "#818cf8", width: 14, height: 14 }
556
+ : e.markerEnd,
557
+ };
558
+ }), [rawEdges, causalNodeIds]);
559
+ const onNodeClick = useCallback((_event, node) => {
560
+ const d = node.data;
561
+ if (d.nodeKind === "event-type") {
562
+ if (flowSelection?.kind === "event-type" && flowSelection.name === d.eventName) {
563
+ dispatch({ type: "ui/flow_node_selected", payload: null });
564
+ }
565
+ else {
566
+ dispatch({
567
+ type: "ui/flow_node_selected",
568
+ payload: { kind: "event-type", name: d.eventName },
569
+ });
570
+ }
571
+ }
572
+ else if (d.nodeKind === "reactor") {
573
+ if (flowSelection?.kind === "reactor" && flowSelection.reactorId === d.reactorId) {
574
+ dispatch({ type: "ui/flow_node_selected", payload: null });
575
+ }
576
+ else {
577
+ dispatch({
578
+ type: "ui/flow_node_selected",
579
+ payload: { kind: "reactor", reactorId: d.reactorId },
580
+ });
581
+ dispatch({ type: "ui/handler_selected", payload: { reactorId: d.reactorId } });
582
+ }
583
+ }
584
+ }, [flowSelection, dispatch]);
585
+ const onPaneClick = useCallback(() => {
586
+ dispatch({ type: "ui/flow_node_selected", payload: null });
587
+ }, [dispatch]);
588
+ const onNodesChange = useCallback((_changes) => { }, []);
589
+ if (!flowCorrelationId) {
590
+ return (_jsx("div", { className: "flex items-center justify-center h-full text-xs text-muted-foreground/50 tracking-wide", children: "Select an event to visualize its causal flow" }));
591
+ }
592
+ if (flowLoading) {
593
+ return (_jsxs("div", { className: "h-full flex flex-col", children: [_jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 border-b border-border shrink-0", children: [_jsx("div", { className: "h-3 w-10 bg-muted rounded animate-pulse" }), _jsx("div", { className: "h-3 w-48 bg-muted rounded animate-pulse" })] }), _jsx("div", { className: "flex-1 flex items-center justify-center", children: _jsxs("div", { className: "animate-pulse flex flex-col items-center gap-3", children: [_jsx("div", { className: "h-8 w-40 bg-muted rounded-md" }), _jsx("div", { className: "h-6 w-px bg-muted" }), _jsx("div", { className: "h-6 w-28 bg-muted rounded-full" }), _jsxs("div", { className: "flex items-start gap-8", children: [_jsxs("div", { className: "flex flex-col items-center gap-3", children: [_jsx("div", { className: "h-6 w-px bg-muted" }), _jsx("div", { className: "h-8 w-36 bg-muted rounded-md" })] }), _jsxs("div", { className: "flex flex-col items-center gap-3", children: [_jsx("div", { className: "h-6 w-px bg-muted" }), _jsx("div", { className: "h-8 w-36 bg-muted rounded-md" })] })] })] }) })] }));
594
+ }
595
+ return (_jsxs("div", { className: "h-full flex flex-col", children: [_jsxs("div", { className: "flex items-center gap-2.5 px-3 py-2 border-b border-border shrink-0", style: { background: "rgba(15, 15, 20, 0.6)", backdropFilter: "blur(8px)" }, children: [_jsx("h3", { className: "text-[10px] font-semibold text-muted-foreground/60 uppercase tracking-widest", children: "Flow" }), _jsx("span", { className: "text-[10px] font-mono text-foreground/80 truncate px-1.5 py-0.5 rounded bg-white/[0.03] border border-border", children: flowCorrelationId }), _jsxs("span", { className: "text-[10px] text-muted-foreground/50 tabular-nums", children: [flowData.length, " events \u00B7 ", nodes.length, " nodes"] }), headerExtra, _jsx("div", { className: "ml-auto", children: _jsx(ReactorFilter, { allReactorIds: allReactorIds, hiddenReactors: hiddenReactors, setHiddenReactors: setHiddenReactors }) })] }), _jsx("div", { className: "flex-1 relative", children: _jsxs(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: nodeTypes, defaultEdgeOptions: { type: "smoothstep" }, onNodesChange: onNodesChange, onNodeClick: onNodeClick, onPaneClick: onPaneClick, minZoom: 0.25, proOptions: { hideAttribution: true }, nodesDraggable: false, nodesConnectable: false, elevateNodesOnSelect: false, colorMode: "dark", children: [_jsx(FitOnLoad, {}), _jsx(FocusOnSelection, { nodes: nodes, flowData: flowData }), _jsx(Background, { color: "rgba(255,255,255,0.03)", gap: 24, size: 1 }), _jsx(Controls, { showInteractive: false })] }) })] }));
596
+ }
@@ -0,0 +1,5 @@
1
+ import type { InspectorEvent } from "../types";
2
+ export type CausalTreePaneProps = {
3
+ onInvestigate?: (event: InspectorEvent) => void;
4
+ };
5
+ export declare function CausalTreePane({ onInvestigate }?: CausalTreePaneProps): import("react/jsx-runtime").JSX.Element;