causal-inspector 0.1.6 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +71 -0
  2. package/dist/CausalInspector.css +20 -447
  3. package/dist/CausalInspector.d.ts +8 -1
  4. package/dist/CausalInspector.js +32 -9
  5. package/dist/causal-inspector.css +2899 -0
  6. package/dist/components/CopyablePayload.js +8 -8
  7. package/dist/components/EffectList.d.ts +4 -0
  8. package/dist/components/EffectList.js +15 -0
  9. package/dist/components/FilterBar.js +7 -10
  10. package/dist/components/GlobalScrubber.js +6 -6
  11. package/dist/components/JsonSyntax.js +8 -8
  12. package/dist/engines/query.js +131 -52
  13. package/dist/engines/scrubber.js +1 -1
  14. package/dist/engines/url.d.ts +5 -2
  15. package/dist/engines/url.js +50 -22
  16. package/dist/events.d.ts +39 -13
  17. package/dist/index.d.ts +5 -3
  18. package/dist/index.js +4 -2
  19. package/dist/panes/AggregateTimelinePane.js +4 -4
  20. package/dist/panes/CausalFlowPane.js +39 -27
  21. package/dist/panes/CausalTreePane.js +43 -34
  22. package/dist/panes/LogsPane.js +9 -17
  23. package/dist/panes/SubjectChainPane.d.ts +1 -0
  24. package/dist/panes/SubjectChainPane.js +50 -0
  25. package/dist/panes/TimelinePane.js +33 -19
  26. package/dist/panes/WaterfallPane.js +5 -5
  27. package/dist/panes/WorkflowExplorerPane.d.ts +2 -0
  28. package/dist/panes/WorkflowExplorerPane.js +49 -0
  29. package/dist/queries.d.ts +16 -12
  30. package/dist/queries.js +103 -27
  31. package/dist/reducer.js +134 -38
  32. package/dist/state.d.ts +18 -5
  33. package/dist/state.js +17 -8
  34. package/dist/theme.js +4 -4
  35. package/dist/types.d.ts +52 -12
  36. package/dist/utils.js +1 -1
  37. package/package.json +18 -3
  38. package/dist/panes/CorrelationExplorerPane.d.ts +0 -2
  39. package/dist/panes/CorrelationExplorerPane.js +0 -51
package/dist/events.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { BaseEvent } from "./machine";
2
- import type { InspectorEvent, CorrelationSummary, ReactorDependency, AggregateLifecycleEntry, FilterState, FlowSelection, ReactorDescription, ReactorDescriptionSnapshot, AggregateTimelineEntry, ReactorLog, ReactorOutcome, ReactorAttempt, PaneLayout } from "./types";
2
+ import type { InspectorEvent, WorkflowSummary, ReactorDependency, AggregateLifecycleEntry, FilterState, FlowSelection, ReactorDescription, ReactorDescriptionSnapshot, AggregateTimelineEntry, ReactorLog, ReactorOutcome, ReactorAttempt, PaneLayout, SubjectChainEvent, SubjectChainMode, InspectorEffect } from "./types";
3
3
  type EventsReceived = BaseEvent<"events/received", InspectorEvent[]>;
4
4
  type SubscriptionConnected = BaseEvent<"events/subscription_connected">;
5
5
  type SubscriptionError = BaseEvent<"events/subscription_error", {
@@ -16,27 +16,27 @@ type CausalTreeLoaded = BaseEvent<"events/causal_tree_loaded", {
16
16
  type FlowLoaded = BaseEvent<"events/flow_loaded", InspectorEvent[]>;
17
17
  type LogsLoaded = BaseEvent<"events/logs_loaded", ReactorLog[]>;
18
18
  type DescriptionsLoaded = BaseEvent<"events/descriptions_loaded", {
19
- correlationId: string;
19
+ workflowId: string;
20
20
  descriptions: ReactorDescription[];
21
21
  }>;
22
22
  type DescriptionSnapshotsLoaded = BaseEvent<"events/description_snapshots_loaded", {
23
- correlationId: string;
23
+ workflowId: string;
24
24
  snapshots: ReactorDescriptionSnapshot[];
25
25
  }>;
26
26
  type AggregateTimelineLoaded = BaseEvent<"events/aggregate_timeline_loaded", {
27
- correlationId: string;
27
+ workflowId: string;
28
28
  entries: AggregateTimelineEntry[];
29
29
  }>;
30
30
  type OutcomesLoaded = BaseEvent<"events/outcomes_loaded", {
31
- correlationId: string;
31
+ workflowId: string;
32
32
  outcomes: ReactorOutcome[];
33
33
  }>;
34
34
  type AttemptsLoaded = BaseEvent<"events/attempts_loaded", {
35
- correlationId: string;
35
+ workflowId: string;
36
36
  attempts: ReactorAttempt[];
37
37
  }>;
38
- type CorrelationsLoaded = BaseEvent<"events/correlations_loaded", {
39
- correlations: CorrelationSummary[];
38
+ type WorkflowsLoaded = BaseEvent<"events/workflows_loaded", {
39
+ workflows: WorkflowSummary[];
40
40
  hasMore: boolean;
41
41
  append: boolean;
42
42
  }>;
@@ -51,7 +51,8 @@ type EventSelected = BaseEvent<"ui/event_selected", {
51
51
  }>;
52
52
  type EventDeselected = BaseEvent<"ui/event_deselected">;
53
53
  type FlowOpened = BaseEvent<"ui/flow_opened", {
54
- correlationId: string;
54
+ workflowId: string;
55
+ focusError?: boolean;
55
56
  }>;
56
57
  type FlowClosed = BaseEvent<"ui/flow_closed">;
57
58
  type FlowNodeSelected = BaseEvent<"ui/flow_node_selected", FlowSelection>;
@@ -68,7 +69,7 @@ type ScrubberPlayToggled = BaseEvent<"ui/scrubber_play_toggled">;
68
69
  type ScrubberSpeedChanged = BaseEvent<"ui/scrubber_speed_changed", {
69
70
  speed: number;
70
71
  }>;
71
- type CorrelationsRequested = BaseEvent<"ui/correlations_requested", {
72
+ type WorkflowsRequested = BaseEvent<"ui/workflows_requested", {
72
73
  search?: string;
73
74
  }>;
74
75
  type HandlerSelected = BaseEvent<"ui/handler_selected", {
@@ -77,10 +78,35 @@ type HandlerSelected = BaseEvent<"ui/handler_selected", {
77
78
  type AggregateLifecycleRequested = BaseEvent<"ui/aggregate_lifecycle_requested", {
78
79
  aggregateKey: string;
79
80
  }>;
80
- type LoadMoreCorrelationsRequested = BaseEvent<"ui/load_more_correlations_requested">;
81
+ type LoadMoreWorkflowsRequested = BaseEvent<"ui/load_more_workflows_requested">;
81
82
  type LocationChanged = BaseEvent<"location/changed", {
82
- correlationId: string | null;
83
+ workflowId: string | null;
83
84
  handler: string | null;
85
+ subject: string | null;
86
+ subjectMode: SubjectChainMode | null;
84
87
  }>;
85
- export type InspectorMachineEvent = EventsReceived | SubscriptionConnected | SubscriptionError | PageLoaded | CausalTreeLoaded | FlowLoaded | LogsLoaded | DescriptionsLoaded | DescriptionSnapshotsLoaded | AggregateTimelineLoaded | OutcomesLoaded | AttemptsLoaded | CorrelationsLoaded | EventSelected | EventDeselected | FlowOpened | FlowClosed | FlowNodeSelected | FilterChanged | LoadMoreRequested | LayoutChanged | ScrubberStartChanged | ScrubberEndChanged | ScrubberPlayToggled | ScrubberSpeedChanged | CorrelationsRequested | ReactorDependenciesLoaded | AggregateKeysLoaded | HandlerSelected | LocationChanged | AggregateLifecycleLoaded | AggregateLifecycleRequested | LoadMoreCorrelationsRequested;
88
+ type SubjectSelected = BaseEvent<"ui/subject_selected", {
89
+ aggregateType: string;
90
+ aggregateId: string;
91
+ mode?: SubjectChainMode;
92
+ }>;
93
+ type SubjectModeChanged = BaseEvent<"ui/subject_mode_changed", {
94
+ mode: SubjectChainMode;
95
+ }>;
96
+ type SubjectChainLoadMore = BaseEvent<"ui/subject_chain_load_more">;
97
+ type EventEffectsRequested = BaseEvent<"ui/event_effects_requested", {
98
+ eventId: string;
99
+ }>;
100
+ type SubjectChainLoaded = BaseEvent<"events/subject_chain_loaded", {
101
+ events: SubjectChainEvent[];
102
+ hasMore: boolean;
103
+ cursor: number | null;
104
+ depthCapped: boolean;
105
+ append: boolean;
106
+ }>;
107
+ type EventEffectsLoaded = BaseEvent<"events/event_effects_loaded", {
108
+ eventId: string;
109
+ effects: InspectorEffect[];
110
+ }>;
111
+ export type InspectorMachineEvent = EventsReceived | SubscriptionConnected | SubscriptionError | PageLoaded | CausalTreeLoaded | FlowLoaded | LogsLoaded | DescriptionsLoaded | DescriptionSnapshotsLoaded | AggregateTimelineLoaded | OutcomesLoaded | AttemptsLoaded | WorkflowsLoaded | EventSelected | EventDeselected | FlowOpened | FlowClosed | FlowNodeSelected | FilterChanged | LoadMoreRequested | LayoutChanged | ScrubberStartChanged | ScrubberEndChanged | ScrubberPlayToggled | ScrubberSpeedChanged | WorkflowsRequested | ReactorDependenciesLoaded | AggregateKeysLoaded | HandlerSelected | LocationChanged | AggregateLifecycleLoaded | AggregateLifecycleRequested | LoadMoreWorkflowsRequested | SubjectSelected | SubjectModeChanged | SubjectChainLoadMore | EventEffectsRequested | SubjectChainLoaded | EventEffectsLoaded;
86
112
  export {};
package/dist/index.d.ts CHANGED
@@ -5,10 +5,10 @@ export { reducer } from "./reducer";
5
5
  export { initialState } from "./state";
6
6
  export type { InspectorState } from "./state";
7
7
  export type { InspectorMachineEvent } from "./events";
8
- export type { InspectorEvent, InspectorEventsPage, InspectorCausalTree, InspectorCausalFlow, CorrelationSummary, ReactorDependency, AggregateLifecycleEntry, Block, ReactorLog, ReactorDescription, ReactorDescriptionSnapshot, AggregateStateEntry, AggregateTimelineEntry, ReactorOutcome, ReactorAttempt, FilterState, LogsFilter, FlowSelection, PaneLayout, } from "./types";
8
+ export type { InspectorEvent, InspectorEventsPage, InspectorCausalTree, InspectorCausalFlow, WorkflowSummary, ReactorDependency, AggregateLifecycleEntry, Block, ReactorLog, ReactorDescription, ReactorDescriptionSnapshot, AggregateStateEntry, AggregateTimelineEntry, ReactorOutcome, ReactorAttempt, FilterState, LogsFilter, FlowSelection, PaneLayout, SubjectChainSourceMode, SubjectChainMode, SubjectChainEvent, InspectorEffect, AggregateKeyEntry, AggregateKeysPage, } from "./types";
9
9
  export { createInspectorEngine, createSubscriptionEngine, createQueryEngine, createStorageEngine, } from "./engines";
10
10
  export type { InspectorTransport, SubscriptionTransport, QueryTransport, StorageTransport, } from "./engines";
11
- export { EVENTS_SUBSCRIPTION, INSPECTOR_EVENTS, INSPECTOR_CAUSAL_TREE, INSPECTOR_CAUSAL_FLOW, INSPECTOR_CORRELATIONS, INSPECTOR_REACTOR_LOGS, INSPECTOR_REACTOR_LOGS_BY_CORRELATION, INSPECTOR_REACTOR_DESCRIPTIONS, INSPECTOR_REACTOR_DESCRIPTION_SNAPSHOTS, INSPECTOR_AGGREGATE_TIMELINE, INSPECTOR_REACTOR_OUTCOMES, INSPECTOR_REACTOR_ATTEMPTS, INSPECTOR_REACTOR_DEPENDENCIES, INSPECTOR_AGGREGATE_KEYS, INSPECTOR_AGGREGATE_LIFECYCLE, } from "./queries";
11
+ export { EVENTS_SUBSCRIPTION, INSPECTOR_EVENTS, INSPECTOR_CAUSAL_TREE, INSPECTOR_CAUSAL_FLOW, INSPECTOR_WORKFLOWS, INSPECTOR_REACTOR_LOGS, INSPECTOR_REACTOR_LOGS_BY_WORKFLOW, INSPECTOR_REACTOR_DESCRIPTIONS, INSPECTOR_REACTOR_DESCRIPTION_SNAPSHOTS, INSPECTOR_AGGREGATE_TIMELINE, INSPECTOR_REACTOR_OUTCOMES, INSPECTOR_REACTOR_ATTEMPTS, INSPECTOR_REACTOR_DEPENDENCIES, INSPECTOR_AGGREGATE_KEYS, INSPECTOR_AGGREGATE_LIFECYCLE, INSPECTOR_SUBJECT_CHAIN, INSPECTOR_EFFECTS_FOR_EVENT, INSPECTOR_AGGREGATE_TYPES, INSPECTOR_AGGREGATE_KEYS_BY_TYPE, } from "./queries";
12
12
  export { eventHue, eventBg, eventBorder, eventTextColor, LOG_LEVEL_COLORS, } from "./theme";
13
13
  export { formatTs, compactPayload, copyToClipboard } from "./utils";
14
14
  export { CausalInspector } from "./CausalInspector";
@@ -17,6 +17,7 @@ export { FilterBar } from "./components/FilterBar";
17
17
  export { CopyablePayload } from "./components/CopyablePayload";
18
18
  export { JsonSyntax } from "./components/JsonSyntax";
19
19
  export { GlobalScrubber } from "./components/GlobalScrubber";
20
+ export { EffectList } from "./components/EffectList";
20
21
  export { TimelinePane } from "./panes/TimelinePane";
21
22
  export type { TimelinePaneProps } from "./panes/TimelinePane";
22
23
  export { CausalTreePane } from "./panes/CausalTreePane";
@@ -27,4 +28,5 @@ export { CausalFlowPane } from "./panes/CausalFlowPane";
27
28
  export type { CausalFlowPaneProps } from "./panes/CausalFlowPane";
28
29
  export { AggregateTimelinePane } from "./panes/AggregateTimelinePane";
29
30
  export { WaterfallPane } from "./panes/WaterfallPane";
30
- export { CorrelationExplorerPane } from "./panes/CorrelationExplorerPane";
31
+ export { WorkflowExplorerPane } from "./panes/WorkflowExplorerPane";
32
+ export { SubjectChainPane } from "./panes/SubjectChainPane";
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ export { initialState } from "./state";
7
7
  // ── Engines ──
8
8
  export { createInspectorEngine, createSubscriptionEngine, createQueryEngine, createStorageEngine, } from "./engines";
9
9
  // ── Queries ──
10
- export { EVENTS_SUBSCRIPTION, INSPECTOR_EVENTS, INSPECTOR_CAUSAL_TREE, INSPECTOR_CAUSAL_FLOW, INSPECTOR_CORRELATIONS, INSPECTOR_REACTOR_LOGS, INSPECTOR_REACTOR_LOGS_BY_CORRELATION, INSPECTOR_REACTOR_DESCRIPTIONS, INSPECTOR_REACTOR_DESCRIPTION_SNAPSHOTS, INSPECTOR_AGGREGATE_TIMELINE, INSPECTOR_REACTOR_OUTCOMES, INSPECTOR_REACTOR_ATTEMPTS, INSPECTOR_REACTOR_DEPENDENCIES, INSPECTOR_AGGREGATE_KEYS, INSPECTOR_AGGREGATE_LIFECYCLE, } from "./queries";
10
+ export { EVENTS_SUBSCRIPTION, INSPECTOR_EVENTS, INSPECTOR_CAUSAL_TREE, INSPECTOR_CAUSAL_FLOW, INSPECTOR_WORKFLOWS, INSPECTOR_REACTOR_LOGS, INSPECTOR_REACTOR_LOGS_BY_WORKFLOW, INSPECTOR_REACTOR_DESCRIPTIONS, INSPECTOR_REACTOR_DESCRIPTION_SNAPSHOTS, INSPECTOR_AGGREGATE_TIMELINE, INSPECTOR_REACTOR_OUTCOMES, INSPECTOR_REACTOR_ATTEMPTS, INSPECTOR_REACTOR_DEPENDENCIES, INSPECTOR_AGGREGATE_KEYS, INSPECTOR_AGGREGATE_LIFECYCLE, INSPECTOR_SUBJECT_CHAIN, INSPECTOR_EFFECTS_FOR_EVENT, INSPECTOR_AGGREGATE_TYPES, INSPECTOR_AGGREGATE_KEYS_BY_TYPE, } from "./queries";
11
11
  // ── Theme ──
12
12
  export { eventHue, eventBg, eventBorder, eventTextColor, LOG_LEVEL_COLORS, } from "./theme";
13
13
  // ── Utilities ──
@@ -19,6 +19,7 @@ export { FilterBar } from "./components/FilterBar";
19
19
  export { CopyablePayload } from "./components/CopyablePayload";
20
20
  export { JsonSyntax } from "./components/JsonSyntax";
21
21
  export { GlobalScrubber } from "./components/GlobalScrubber";
22
+ export { EffectList } from "./components/EffectList";
22
23
  // ── Panes ──
23
24
  export { TimelinePane } from "./panes/TimelinePane";
24
25
  export { CausalTreePane } from "./panes/CausalTreePane";
@@ -26,4 +27,5 @@ export { LogsPane } from "./panes/LogsPane";
26
27
  export { CausalFlowPane } from "./panes/CausalFlowPane";
27
28
  export { AggregateTimelinePane } from "./panes/AggregateTimelinePane";
28
29
  export { WaterfallPane } from "./panes/WaterfallPane";
29
- export { CorrelationExplorerPane } from "./panes/CorrelationExplorerPane";
30
+ export { WorkflowExplorerPane } from "./panes/WorkflowExplorerPane";
31
+ export { SubjectChainPane } from "./panes/SubjectChainPane";
@@ -144,8 +144,8 @@ function AggregateCard({ aggregateKey, state, prevState, diffMode, }) {
144
144
  }, children: [_jsx("span", { style: { fontSize: 9, color: "#52525b", transform: collapsed ? "rotate(-90deg)" : "rotate(0deg)", transition: "transform 100ms" }, children: "\u25BC" }), _jsx("span", { style: { fontSize: 11, fontWeight: 500, color: "#e4e4e7" }, children: aggType }), aggId && (_jsx("span", { style: { fontSize: 10, color: "#71717a", fontFamily: "monospace" }, children: aggId })), showDiff && diffs.length > 0 && (_jsxs("span", { style: { fontSize: 9, color: "#fbbf24", marginLeft: "auto" }, children: [diffs.length, " change", diffs.length !== 1 ? "s" : ""] })), showDiff && diffs.length === 0 && (_jsx("span", { style: { fontSize: 9, color: "#52525b", marginLeft: "auto" }, children: "unchanged" }))] }), !collapsed && (_jsx("div", { style: { padding: "4px 8px 6px" }, children: showDiff ? _jsx(DiffView, { diffs: diffs }) : _jsx(InlineJson, { value: state }) }))] }));
145
145
  }
146
146
  export function AggregateTimelinePane() {
147
- const correlationId = useSelector((s) => s.flowCorrelationId);
148
- const entries = useSelector((s) => correlationId ? s.aggregateTimeline[correlationId] ?? [] : []);
147
+ const workflowId = useSelector((s) => s.flowWorkflowId);
148
+ const entries = useSelector((s) => workflowId ? s.aggregateTimeline[workflowId] ?? [] : []);
149
149
  const scrubberStart = useSelector((s) => s.scrubberStart);
150
150
  const scrubberEnd = useSelector((s) => s.scrubberEnd);
151
151
  const logsFilter = useSelector((s) => s.logsFilter);
@@ -185,11 +185,11 @@ export function AggregateTimelinePane() {
185
185
  const handleRowClick = useCallback((seq) => {
186
186
  dispatch({ type: "ui/scrubber_end_changed", payload: { end: seq } });
187
187
  }, [dispatch]);
188
- if (!correlationId) {
188
+ if (!workflowId) {
189
189
  return (_jsx("div", { style: { height: "100%", display: "flex", alignItems: "center", justifyContent: "center", color: "#50506a", fontSize: 12, letterSpacing: "0.03em" }, children: "Open a flow to see the aggregate state timeline" }));
190
190
  }
191
191
  if (entries.length === 0) {
192
- return (_jsx("div", { style: { height: "100%", display: "flex", alignItems: "center", justifyContent: "center", color: "#50506a", fontSize: 12, letterSpacing: "0.03em" }, children: "No aggregate state snapshots for this correlation" }));
192
+ return (_jsx("div", { style: { height: "100%", display: "flex", alignItems: "center", justifyContent: "center", color: "#50506a", fontSize: 12, letterSpacing: "0.03em" }, children: "No aggregate state snapshots for this workflow" }));
193
193
  }
194
194
  return (_jsxs("div", { style: { height: "100%", display: "flex", flexDirection: "column" }, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4, padding: "6px 12px", borderBottom: "1px solid rgba(255,255,255,0.06)", flexShrink: 0, background: "rgba(15, 15, 20, 0.6)", backdropFilter: "blur(8px)" }, children: [_jsx("button", { onClick: () => setDiffMode(false), style: {
195
195
  fontSize: 10,
@@ -126,10 +126,10 @@ function buildFlowGraph(events, descriptions, outcomes, hiddenReactors, directio
126
126
  children.add(groupKey);
127
127
  reactorToChildTypes.set(evt.reactorId, children);
128
128
  }
129
- if (evt.parentId && evt.reactorId) {
130
- const reactors = parentToReactor.get(evt.parentId) ?? new Set();
129
+ if (evt.causationId && evt.reactorId) {
130
+ const reactors = parentToReactor.get(evt.causationId) ?? new Set();
131
131
  reactors.add(evt.reactorId);
132
- parentToReactor.set(evt.parentId, reactors);
132
+ parentToReactor.set(evt.causationId, reactors);
133
133
  }
134
134
  }
135
135
  const eventIdToGroup = new Map();
@@ -334,8 +334,8 @@ function computeVisibleIds(allEvents, start, end) {
334
334
  edgeIds.add(`hdl:${evt.reactorId}->evt:${evt.name}`);
335
335
  }
336
336
  // Parent event -> reactor edge
337
- if (evt.parentId && evt.reactorId) {
338
- const parentGroup = eventIdToGroup.get(evt.parentId);
337
+ if (evt.causationId && evt.reactorId) {
338
+ const parentGroup = eventIdToGroup.get(evt.causationId);
339
339
  if (parentGroup) {
340
340
  edgeIds.add(`evt:${parentGroup}->hdl:${evt.reactorId}`);
341
341
  }
@@ -343,7 +343,7 @@ function computeVisibleIds(allEvents, start, end) {
343
343
  // Root event -> reactor edges
344
344
  if (!evt.reactorId && evt.id) {
345
345
  for (const child of visible) {
346
- if (child.parentId === evt.id && child.reactorId) {
346
+ if (child.causationId === evt.id && child.reactorId) {
347
347
  const rootGroup = eventIdToGroup.get(evt.id);
348
348
  if (rootGroup) {
349
349
  edgeIds.add(`evt:${rootGroup}->hdl:${child.reactorId}`);
@@ -373,9 +373,19 @@ function FitOnLoad() {
373
373
  function FocusOnSelection({ nodes, flowData }) {
374
374
  const selectedSeq = useSelector((s) => s.selectedSeq);
375
375
  const scrubberEnd = useSelector((s) => s.scrubberEnd);
376
+ const flowSelection = useSelector((s) => s.flowSelection);
376
377
  const { setCenter, getZoom } = useReactFlow();
377
378
  const nodesRef = useRef(nodes);
378
379
  nodesRef.current = nodes;
380
+ const centerOnNode = useCallback((nodeId) => {
381
+ const node = nodesRef.current.find(n => n.id === nodeId);
382
+ if (!node)
383
+ return;
384
+ const isReactor = node.id.startsWith("hdl:");
385
+ const w = isReactor ? REACTOR_WIDTH : NODE_WIDTH;
386
+ const h = isReactor ? estimateReactorHeight(node.data) : NODE_HEIGHT;
387
+ setCenter(node.position.x + w / 2, node.position.y + h / 2, { zoom: getZoom(), duration: 400 });
388
+ }, [setCenter, getZoom]);
379
389
  useEffect(() => {
380
390
  // Don't recenter while scrubber is active
381
391
  if (scrubberEnd != null)
@@ -385,15 +395,17 @@ function FocusOnSelection({ nodes, flowData }) {
385
395
  const evt = flowData.find(e => e.seq === selectedSeq);
386
396
  if (!evt)
387
397
  return;
388
- const nodeId = `evt:${evt.name}`;
389
- const node = nodesRef.current.find(n => n.id === nodeId);
390
- if (!node)
398
+ centerOnNode(`evt:${evt.name}`);
399
+ }, [selectedSeq, scrubberEnd, flowData, centerOnNode]);
400
+ // Pan to a selected reactor node (e.g. the failed reactor opened from the
401
+ // Workflows error pill), so it isn't left off-screen in a large graph.
402
+ useEffect(() => {
403
+ if (scrubberEnd != null)
391
404
  return;
392
- const isReactor = node.id.startsWith("hdl:");
393
- const w = isReactor ? REACTOR_WIDTH : NODE_WIDTH;
394
- const h = isReactor ? estimateReactorHeight(node.data) : NODE_HEIGHT;
395
- setCenter(node.position.x + w / 2, node.position.y + h / 2, { zoom: getZoom(), duration: 400 });
396
- }, [selectedSeq, scrubberEnd, flowData, setCenter, getZoom]);
405
+ if (flowSelection?.kind !== "reactor" || !flowData.length)
406
+ return;
407
+ centerOnNode(`hdl:${flowSelection.reactorId}`);
408
+ }, [flowSelection, scrubberEnd, flowData, centerOnNode]);
397
409
  return null;
398
410
  }
399
411
  // ---------------------------------------------------------------------------
@@ -427,10 +439,10 @@ function ReactorFilter({ allReactorIds, hiddenReactors, setHiddenReactors }) {
427
439
  ? allReactorIds.filter(id => id.toLowerCase().includes(filter.toLowerCase()))
428
440
  : allReactorIds;
429
441
  const hiddenCount = hiddenReactors.size;
430
- return (_jsxs("div", { ref: containerRef, style: { position: "relative" }, children: [_jsxs("button", { onClick: () => setOpen(v => !v), className: "ci-toolbar-btn", children: [_jsx(Filter, { size: 11, style: { display: "inline", marginRight: 4, verticalAlign: "-1px" } }), hiddenCount > 0 ? `${hiddenCount} hidden` : "Filter"] }), open && (_jsxs("div", { className: "ci-dropdown", style: { position: "absolute", top: "100%", right: 0, marginTop: 4, zIndex: 50, minWidth: 240 }, children: [_jsx("div", { style: { padding: "8px 12px", borderBottom: "1px solid var(--ci-border)" }, children: _jsx("input", { autoFocus: true, type: "text", value: filter, onChange: e => setFilter(e.target.value), placeholder: "Search reactors...", style: { width: "100%", fontSize: 12, background: "transparent", border: "none", outline: "none", color: "var(--ci-text)" } }) }), _jsxs("div", { style: { maxHeight: 256, overflowY: "auto", padding: "4px 0" }, children: [filtered.map(id => (_jsxs("label", { className: "ci-row", style: { gap: 8, padding: "6px 12px", cursor: "pointer", transition: "background 150ms" }, children: [_jsx("input", { type: "checkbox", checked: !hiddenReactors.has(id), onChange: () => toggle(id), style: { accentColor: "var(--ci-accent)" } }), _jsx("span", { className: "ci-mono ci-truncate", style: { fontSize: 11, color: "var(--ci-text)", opacity: 0.8 }, children: id })] }, id))), filtered.length === 0 && (_jsx("div", { style: { fontSize: 12, color: "var(--ci-text-muted)", opacity: 0.5, padding: "8px 12px" }, children: "No matches" }))] })] }))] }));
442
+ 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" }))] })] }))] }));
431
443
  }
432
444
  export function CausalFlowPane({ defaultHiddenReactors, headerExtra } = {}) {
433
- const flowCorrelationId = useSelector((s) => s.flowCorrelationId);
445
+ const flowWorkflowId = useSelector((s) => s.flowWorkflowId);
434
446
  const flowData = useSelector((s) => s.flowData);
435
447
  const flowSelection = useSelector((s) => s.flowSelection);
436
448
  const descriptionsMap = useSelector((s) => s.descriptions);
@@ -438,30 +450,30 @@ export function CausalFlowPane({ defaultHiddenReactors, headerExtra } = {}) {
438
450
  const scrubberStart = useSelector((s) => s.scrubberStart);
439
451
  const scrubberEnd = useSelector((s) => s.scrubberEnd);
440
452
  const dispatch = useDispatch();
441
- const flowLoading = flowCorrelationId != null && flowData.length === 0;
453
+ const flowLoading = flowWorkflowId != null && flowData.length === 0;
442
454
  // Build typed maps from state
443
455
  const descriptions = useMemo(() => {
444
- if (!flowCorrelationId)
456
+ if (!flowWorkflowId)
445
457
  return undefined;
446
- const raw = descriptionsMap[flowCorrelationId];
458
+ const raw = descriptionsMap[flowWorkflowId];
447
459
  if (!raw)
448
460
  return undefined;
449
461
  const map = new Map();
450
462
  for (const d of raw)
451
463
  map.set(d.reactorId, d.blocks);
452
464
  return map;
453
- }, [descriptionsMap, flowCorrelationId]);
465
+ }, [descriptionsMap, flowWorkflowId]);
454
466
  const outcomes = useMemo(() => {
455
- if (!flowCorrelationId)
467
+ if (!flowWorkflowId)
456
468
  return undefined;
457
- const raw = outcomesMap[flowCorrelationId];
469
+ const raw = outcomesMap[flowWorkflowId];
458
470
  if (!raw)
459
471
  return undefined;
460
472
  const map = new Map();
461
473
  for (const o of raw)
462
474
  map.set(o.reactorId, o);
463
475
  return map;
464
- }, [outcomesMap, flowCorrelationId]);
476
+ }, [outcomesMap, flowWorkflowId]);
465
477
  const [hiddenReactors, setHiddenReactors] = useState(() => defaultHiddenReactors ?? new Set());
466
478
  const [direction, setDirection] = useState("LR");
467
479
  const allReactorIds = useMemo(() => {
@@ -590,11 +602,11 @@ export function CausalFlowPane({ defaultHiddenReactors, headerExtra } = {}) {
590
602
  dispatch({ type: "ui/flow_node_selected", payload: null });
591
603
  }, [dispatch]);
592
604
  const onNodesChange = useCallback((_changes) => { }, []);
593
- if (!flowCorrelationId) {
594
- return (_jsx("div", { className: "ci-empty", children: "Select an event to visualize its causal flow" }));
605
+ if (!flowWorkflowId) {
606
+ 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" }));
595
607
  }
596
608
  if (flowLoading) {
597
- return (_jsxs("div", { className: "ci-col", children: [_jsxs("div", { className: "ci-row", style: { gap: 8, padding: "6px 12px", borderBottom: "1px solid var(--ci-border)", flexShrink: 0 }, children: [_jsx("div", { className: "ci-muted-bg ci-pulse", style: { height: 12, width: 40 } }), _jsx("div", { className: "ci-muted-bg ci-pulse", style: { height: 12, width: 192 } })] }), _jsx("div", { style: { flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }, children: _jsxs("div", { className: "ci-pulse", style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 12 }, children: [_jsx("div", { className: "ci-muted-bg", style: { height: 32, width: 160, borderRadius: "var(--ci-radius-md)" } }), _jsx("div", { className: "ci-muted-bg", style: { height: 24, width: 1 } }), _jsx("div", { className: "ci-muted-bg", style: { height: 24, width: 112, borderRadius: "var(--ci-radius-full)" } }), _jsxs("div", { style: { display: "flex", alignItems: "start", gap: 32 }, children: [_jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 12 }, children: [_jsx("div", { className: "ci-muted-bg", style: { height: 24, width: 1 } }), _jsx("div", { className: "ci-muted-bg", style: { height: 32, width: 144, borderRadius: "var(--ci-radius-md)" } })] }), _jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 12 }, children: [_jsx("div", { className: "ci-muted-bg", style: { height: 24, width: 1 } }), _jsx("div", { className: "ci-muted-bg", style: { height: 32, width: 144, borderRadius: "var(--ci-radius-md)" } })] })] })] }) })] }));
609
+ 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" })] })] })] }) })] }));
598
610
  }
599
- return (_jsxs("div", { className: "ci-col", children: [_jsxs("div", { className: "ci-row ci-surface", style: { gap: 10, padding: "8px 12px", borderBottom: "1px solid var(--ci-border)", flexShrink: 0 }, children: [_jsx("h3", { className: "ci-header", children: "Flow" }), _jsx("span", { className: "ci-mono ci-truncate", style: { fontSize: 10, color: "var(--ci-text)", opacity: 0.8, padding: "2px 6px", borderRadius: "var(--ci-radius-sm)", background: "rgba(255, 255, 255, 0.03)", border: "1px solid var(--ci-border)" }, children: flowCorrelationId }), _jsxs("span", { className: "ci-tabular", style: { fontSize: 10, color: "var(--ci-text-muted)", opacity: 0.5 }, children: [flowData.length, " events \u00B7 ", nodes.length, " nodes"] }), headerExtra, _jsxs("div", { className: "ci-row", style: { marginLeft: "auto", gap: 6 }, children: [_jsx("button", { onClick: () => setDirection(d => d === "LR" ? "TB" : "LR"), className: "ci-toolbar-btn", title: direction === "LR" ? "Switch to vertical layout" : "Switch to horizontal layout", children: direction === "LR" ? _jsx(ArrowDown, { size: 11, style: { display: "inline", verticalAlign: "-1px" } }) : _jsx(ArrowRight, { size: 11, style: { display: "inline", verticalAlign: "-1px" } }) }), _jsx(ReactorFilter, { allReactorIds: allReactorIds, hiddenReactors: hiddenReactors, setHiddenReactors: setHiddenReactors })] })] }), _jsx("div", { style: { flex: 1, position: "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 })] }) })] }));
611
+ return (_jsxs("div", { className: "h-full flex flex-col", children: [_jsxs("div", { className: "relative z-20 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: flowWorkflowId }), _jsxs("span", { className: "text-[10px] text-muted-foreground/50 tabular-nums", children: [flowData.length, " events \u00B7 ", nodes.length, " nodes"] }), headerExtra, _jsxs("div", { className: "ml-auto flex items-center gap-1.5", children: [_jsx("button", { onClick: () => setDirection(d => d === "LR" ? "TB" : "LR"), 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", title: direction === "LR" ? "Switch to vertical layout" : "Switch to horizontal layout", children: direction === "LR" ? _jsx(ArrowDown, { size: 11, className: "inline" }) : _jsx(ArrowRight, { size: 11, className: "inline" }) }), _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 })] }) })] }));
600
612
  }
@@ -2,9 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useState, useMemo, useRef, useEffect, useCallback } from "react";
3
3
  import { useSelector, useDispatch } from "../machine";
4
4
  import { CopyablePayload } from "../components/CopyablePayload";
5
+ import { EffectList } from "../components/EffectList";
5
6
  import { eventTextColor } from "../theme";
6
7
  import { formatTs, compactPayload, copyToClipboard, inScrubberRange } from "../utils";
7
- import { Copy, Check, Search, X, ChevronRight, ChevronDown, AlertTriangle } from "lucide-react";
8
+ import { Copy, Check, Search, X, ChevronRight, ChevronDown, AlertTriangle, Zap } from "lucide-react";
8
9
  function buildTreeJson(roots, childrenMap) {
9
10
  function toNode(evt) {
10
11
  const children = evt.id ? (childrenMap.get(evt.id) ?? []) : [];
@@ -36,17 +37,11 @@ function ReactorNode({ reactorId, parentEventId, children, childrenMap, depth, i
36
37
  }, [onClickReactor, parentEventId, reactorId]);
37
38
  const isError = outcome?.status === "error";
38
39
  const isRunning = outcome?.status === "running";
39
- return (_jsxs("div", { style: depth > 0 ? { paddingLeft: 24 } : undefined, children: [_jsxs("div", { ref: isHighlighted ? nodeRef : undefined, className: `ci-tree-node${isHighlighted ? " ci-selected" : ""}`, style: isError ? { background: "rgba(239, 68, 68, 0.08)" } : undefined, children: [_jsxs("div", { className: "ci-row", style: { gap: 6, minWidth: 0 }, children: [_jsx("button", { onClick: (e) => { e.stopPropagation(); setCollapsed(v => !v); }, style: { fontSize: 10, color: "var(--ci-text-muted)", background: "none", border: "none", cursor: "pointer", flexShrink: 0, width: 12, textAlign: "center", padding: 0, transition: "color 150ms" }, children: collapsed ? _jsx(ChevronRight, { size: 10 }) : _jsx(ChevronDown, { size: 10 }) }), _jsxs("button", { onClick: handleClick, className: "ci-row", style: { gap: 6, minWidth: 0, background: "none", border: "none", cursor: "pointer", padding: 0 }, children: [_jsx("span", { style: {
40
- padding: "2px 6px",
41
- borderRadius: "var(--ci-radius-sm)",
42
- fontSize: 9,
43
- fontWeight: 500,
44
- flexShrink: 0,
45
- fontStyle: "italic",
46
- background: isError ? "rgba(239, 68, 68, 0.1)" : isRunning ? "rgba(234, 179, 8, 0.1)" : "rgba(255, 255, 255, 0.04)",
47
- color: isError ? "rgba(248, 113, 113, 0.8)" : isRunning ? "rgba(234, 179, 8, 0.8)" : "var(--ci-text-dim)",
48
- border: `1px solid ${isError ? "rgba(239, 68, 68, 0.2)" : isRunning ? "rgba(234, 179, 8, 0.2)" : "var(--ci-border)"}`,
49
- }, children: "reactor" }), _jsx("span", { className: "ci-mono", style: { fontSize: 10, color: "var(--ci-text)", opacity: 0.6, flexShrink: 0 }, children: reactorId }), isError && (_jsxs("span", { className: "flex items-center gap-1 text-[9px] text-red-400/80 shrink-0", title: outcome.error ?? "Error", children: [_jsx(AlertTriangle, { size: 10 }), outcome.attempts > 1 && _jsxs("span", { children: ["x", outcome.attempts] })] })), collapsed && (_jsxs("span", { style: { fontSize: 10, color: "var(--ci-text-muted)", flexShrink: 0 }, children: ["(", children.length, ")"] }))] })] }), isError && outcome.error && (_jsx("div", { className: "mt-1 ml-7 text-[9px] text-red-400/70 truncate", title: outcome.error, children: outcome.error }))] }), !collapsed && children.map((child) => (_jsx(TreeNode, { event: child, childrenMap: childrenMap, depth: depth + 1, onClickReactor: onClickReactor, outcomesByReactor: outcomesByReactor }, child.seq)))] }));
40
+ return (_jsxs("div", { className: depth > 0 ? "pl-6" : "", children: [_jsxs("div", { ref: isHighlighted ? nodeRef : undefined, className: `group/tree w-full text-left px-2 py-1.5 rounded-md transition-all duration-150 hover:bg-white/[0.03] ${isHighlighted ? "bg-indigo-500/15 ring-1 ring-indigo-500/25" : ""} ${isError ? "bg-red-500/8" : ""}`, children: [_jsxs("div", { className: "flex items-center gap-1.5 min-w-0", children: [_jsx("button", { onClick: (e) => { e.stopPropagation(); setCollapsed(v => !v); }, className: "text-[10px] text-muted-foreground hover:text-foreground shrink-0 w-3 text-center", children: collapsed ? _jsx(ChevronRight, { size: 10 }) : _jsx(ChevronDown, { size: 10 }) }), _jsxs("button", { onClick: handleClick, className: "flex items-center gap-1.5 min-w-0", children: [_jsx("span", { className: `px-1.5 py-0.5 rounded text-[9px] font-medium shrink-0 italic border ${isError
41
+ ? "bg-red-500/10 text-red-400/80 border-red-500/20"
42
+ : isRunning
43
+ ? "bg-yellow-500/10 text-yellow-400/80 border-yellow-500/20"
44
+ : "bg-white/[0.04] text-muted-foreground/60 border-border"}`, children: "reactor" }), _jsx("span", { className: "text-[10px] font-mono text-foreground/60 shrink-0", children: reactorId }), isError && (_jsxs("span", { className: "flex items-center gap-1 text-[9px] text-red-400/80 shrink-0", title: outcome.error ?? "Error", children: [_jsx(AlertTriangle, { size: 10 }), outcome.attempts > 1 && _jsxs("span", { children: ["x", outcome.attempts] })] })), collapsed && (_jsxs("span", { className: "text-[10px] text-muted-foreground shrink-0", children: ["(", children.length, ")"] }))] })] }), isError && outcome.error && (_jsx("div", { className: "mt-1 ml-7 text-[9px] text-red-400/70 truncate", title: outcome.error, children: outcome.error }))] }), !collapsed && children.map((child) => (_jsx(TreeNode, { event: child, childrenMap: childrenMap, depth: depth + 1, onClickReactor: onClickReactor, outcomesByReactor: outcomesByReactor }, child.seq)))] }));
50
45
  }
51
46
  // ---------------------------------------------------------------------------
52
47
  // TreeNode (recursive)
@@ -54,8 +49,11 @@ function ReactorNode({ reactorId, parentEventId, children, childrenMap, depth, i
54
49
  function TreeNode({ event, childrenMap, depth, onClickReactor, onInvestigate, outcomesByReactor, }) {
55
50
  const selectedSeq = useSelector((s) => s.selectedSeq);
56
51
  const flowSelection = useSelector((s) => s.flowSelection);
52
+ const expandedEffects = useSelector((s) => s.expandedEffects);
53
+ const loadingEffectsIds = useSelector((s) => s.loadingEffects);
57
54
  const dispatch = useDispatch();
58
55
  const [payloadOpen, setPayloadOpen] = useState(false);
56
+ const [effectsOpen, setEffectsOpen] = useState(false);
59
57
  const [collapsed, setCollapsed] = useState(false);
60
58
  const [copied, setCopied] = useState(false);
61
59
  const isSelected = event.seq === selectedSeq;
@@ -84,19 +82,30 @@ function TreeNode({ event, childrenMap, depth, onClickReactor, onInvestigate, ou
84
82
  return { reactorGroups: groups, directChildren: direct };
85
83
  }, [children]);
86
84
  const highlightedReactorId = flowSelection?.kind === "reactor" ? flowSelection.reactorId : null;
87
- return (_jsxs("div", { style: depth > 0 ? { paddingLeft: 24 } : undefined, children: [_jsxs("div", { ref: isSelected ? nodeRef : undefined, onClick: () => {
85
+ return (_jsxs("div", { className: depth > 0 ? "pl-6" : "", children: [_jsxs("div", { ref: isSelected ? nodeRef : undefined, onClick: () => {
88
86
  dispatch({ type: "ui/event_selected", payload: { seq: event.seq } });
89
- if (event.correlationId) {
90
- dispatch({ type: "ui/flow_opened", payload: { correlationId: event.correlationId } });
87
+ if (event.workflowId) {
88
+ dispatch({ type: "ui/flow_opened", payload: { workflowId: event.workflowId } });
91
89
  }
92
- }, className: `ci-tree-node${isSelected ? " ci-selected" : ""}`, style: { cursor: "pointer" }, children: [_jsxs("div", { className: "ci-row", style: { gap: 6, minWidth: 0 }, children: [hasChildren ? (_jsx("button", { onClick: (e) => { e.stopPropagation(); setCollapsed((v) => !v); }, style: { fontSize: 10, color: "var(--ci-text-muted)", background: "none", border: "none", cursor: "pointer", flexShrink: 0, width: 12, textAlign: "center", padding: 0, transition: "color 150ms" }, children: collapsed ? _jsx(ChevronRight, { size: 10 }) : _jsx(ChevronDown, { size: 10 }) })) : (_jsx("span", { style: { width: 12, flexShrink: 0 } })), _jsx("span", { className: "ci-mono", style: { fontSize: 10, flexShrink: 0, color: eventTextColor(event.name) }, children: event.name }), collapsed && hasChildren && (_jsxs("span", { style: { fontSize: 10, color: "var(--ci-text-muted)", flexShrink: 0 }, children: ["(", children.length, ")"] })), _jsx("span", { style: { fontSize: 10, color: "var(--ci-text-muted)", flexShrink: 0 }, children: formatTs(event.ts) }), _jsx("button", { onClick: (e) => {
90
+ }, className: `group/tree w-full text-left px-2 py-1.5 rounded-md transition-all duration-150 cursor-pointer hover:bg-white/[0.03] ${isSelected ? "bg-indigo-500/15 ring-1 ring-indigo-500/25" : ""}`, children: [_jsxs("div", { className: "flex items-center gap-1.5 min-w-0", children: [hasChildren ? (_jsx("button", { onClick: (e) => { e.stopPropagation(); setCollapsed((v) => !v); }, className: "text-[10px] text-muted-foreground hover:text-foreground shrink-0 w-3 text-center", children: collapsed ? _jsx(ChevronRight, { size: 10 }) : _jsx(ChevronDown, { size: 10 }) })) : (_jsx("span", { className: "w-3 shrink-0" })), _jsx("span", { className: "text-[10px] font-mono shrink-0", style: { color: eventTextColor(event.name) }, children: event.name }), collapsed && hasChildren && (_jsxs("span", { className: "text-[10px] text-muted-foreground shrink-0", children: ["(", children.length, ")"] })), event.aggregateType && event.aggregateId && (_jsxs("button", { onClick: (e) => {
91
+ e.stopPropagation();
92
+ dispatch({ type: "ui/subject_selected", payload: { aggregateType: event.aggregateType, aggregateId: event.aggregateId, mode: "both" } });
93
+ }, className: "px-1.5 py-0.5 rounded-full text-[9px] font-mono bg-teal-500/8 text-teal-400/80 hover:bg-teal-500/15 hover:text-teal-400 shrink-0 transition-all border border-teal-500/10", title: `View subject ${event.aggregateType}:${event.aggregateId}`, children: [event.aggregateType, ":", event.aggregateId.slice(0, 8)] })), _jsx("span", { className: "text-[10px] text-muted-foreground shrink-0", children: formatTs(event.ts) }), event.id && (_jsxs("button", { onClick: (e) => {
94
+ e.stopPropagation();
95
+ if (!effectsOpen && expandedEffects[event.id] === undefined) {
96
+ dispatch({ type: "ui/event_effects_requested", payload: { eventId: event.id } });
97
+ }
98
+ setEffectsOpen((v) => !v);
99
+ }, className: `opacity-0 group-hover/tree:opacity-100 transition-all duration-150 flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] ${effectsOpen
100
+ ? "opacity-100 bg-indigo-500/10 text-indigo-400/70 border border-indigo-500/20"
101
+ : "hover:bg-white/[0.05] text-muted-foreground/50 border border-transparent"}`, title: "Show effects", children: [_jsx(Zap, { size: 9 }), expandedEffects[event.id] !== undefined && expandedEffects[event.id].length > 0 && (_jsx("span", { children: expandedEffects[event.id].length }))] })), _jsx("button", { onClick: (e) => {
93
102
  e.stopPropagation();
94
103
  const json = buildTreeJson([event], childrenMap);
95
104
  const text = JSON.stringify(json[0], null, 2);
96
105
  copyToClipboard(text);
97
106
  setCopied(true);
98
107
  setTimeout(() => setCopied(false), 1500);
99
- }, className: "ci-tree-actions ci-btn", style: { marginLeft: "auto", flexShrink: 0, fontSize: 10 }, title: "Copy subtree as JSON", children: copied ? _jsx(Check, { size: 12 }) : _jsx(Copy, { size: 12 }) }), onInvestigate && (_jsx("button", { onClick: (e) => { e.stopPropagation(); onInvestigate(event); }, className: "ci-tree-actions ci-btn", style: { flexShrink: 0 }, title: "Investigate", children: _jsx(Search, { size: 12 }) }))] }), _jsx("button", { onClick: (e) => { e.stopPropagation(); setPayloadOpen((v) => !v); }, className: "ci-mono ci-truncate", style: { marginTop: 2, marginLeft: 12, fontSize: 10, color: "var(--ci-text-muted)", textAlign: "left", maxWidth: "100%", display: "block", background: "none", border: "none", cursor: "pointer", padding: 0, transition: "color 150ms" }, title: "Click to expand payload", children: event.summary ?? compactPayload(event.payload) }), payloadOpen && (_jsx("div", { style: { marginTop: 4, marginLeft: 12, maxHeight: 192 }, children: _jsx(CopyablePayload, { payload: event.payload }) }))] }), !collapsed && (_jsxs(_Fragment, { children: [directChildren.map((child) => (_jsx(TreeNode, { event: child, childrenMap: childrenMap, depth: depth + 1, onClickReactor: onClickReactor, onInvestigate: onInvestigate, outcomesByReactor: outcomesByReactor }, child.seq))), [...reactorGroups.entries()].map(([hid, group]) => (_jsx(ReactorNode, { reactorId: hid, parentEventId: event.id, children: group, childrenMap: childrenMap, depth: depth + 1, isHighlighted: hid === highlightedReactorId, onClickReactor: onClickReactor, outcome: outcomesByReactor?.get(hid), outcomesByReactor: outcomesByReactor }, hid)))] }))] }));
108
+ }, className: "opacity-0 group-hover/tree:opacity-100 transition-all duration-150 ml-auto p-1 rounded-md hover:bg-white/[0.05] shrink-0 text-[10px] text-muted-foreground/50", title: "Copy subtree as JSON", children: copied ? _jsx(Check, { size: 12 }) : _jsx(Copy, { size: 12 }) }), onInvestigate && (_jsx("button", { onClick: (e) => { e.stopPropagation(); onInvestigate(event); }, className: "opacity-0 group-hover/tree:opacity-100 transition-all duration-150 p-1 rounded-md hover:bg-white/[0.05] shrink-0 text-muted-foreground/50", title: "Investigate", children: _jsx(Search, { size: 12 }) }))] }), _jsx("button", { onClick: (e) => { e.stopPropagation(); setPayloadOpen((v) => !v); }, className: "mt-0.5 ml-3 text-[10px] font-mono text-muted-foreground hover:text-foreground truncate text-left max-w-full block", title: "Click to expand payload", children: event.summary ?? compactPayload(event.payload) }), payloadOpen && (_jsx(CopyablePayload, { payload: event.payload, className: "mt-1 ml-3 max-h-48" })), effectsOpen && (_jsx("div", { className: "mt-1 ml-3", children: loadingEffectsIds.includes(event.id ?? "") ? (_jsx("div", { className: "text-[9px] text-muted-foreground/40 italic py-1", children: "Loading\u2026" })) : (_jsx(EffectList, { effects: event.id ? (expandedEffects[event.id] ?? []) : [] })) }))] }), !collapsed && (_jsxs(_Fragment, { children: [directChildren.map((child) => (_jsx(TreeNode, { event: child, childrenMap: childrenMap, depth: depth + 1, onClickReactor: onClickReactor, onInvestigate: onInvestigate, outcomesByReactor: outcomesByReactor }, child.seq))), [...reactorGroups.entries()].map(([hid, group]) => (_jsx(ReactorNode, { reactorId: hid, parentEventId: event.id, children: group, childrenMap: childrenMap, depth: depth + 1, isHighlighted: hid === highlightedReactorId, onClickReactor: onClickReactor, outcome: outcomesByReactor?.get(hid), outcomesByReactor: outcomesByReactor }, hid)))] }))] }));
100
109
  }
101
110
  // ---------------------------------------------------------------------------
102
111
  // matchesFlowSelection
@@ -112,23 +121,23 @@ export function CausalTreePane({ onInvestigate } = {}) {
112
121
  const causalTree = useSelector((s) => s.causalTree);
113
122
  const selectedSeq = useSelector((s) => s.selectedSeq);
114
123
  const flowSelection = useSelector((s) => s.flowSelection);
115
- const flowCorrelationId = useSelector((s) => s.flowCorrelationId);
124
+ const flowWorkflowId = useSelector((s) => s.flowWorkflowId);
116
125
  const scrubberStart = useSelector((s) => s.scrubberStart);
117
126
  const scrubberEnd = useSelector((s) => s.scrubberEnd);
118
127
  const outcomesMap = useSelector((s) => s.outcomes);
119
128
  const dispatch = useDispatch();
120
129
  // Build reactor outcome lookup for current flow
121
130
  const outcomesByReactor = useMemo(() => {
122
- if (!flowCorrelationId)
131
+ if (!flowWorkflowId)
123
132
  return new Map();
124
- const raw = outcomesMap[flowCorrelationId];
133
+ const raw = outcomesMap[flowWorkflowId];
125
134
  if (!raw)
126
135
  return new Map();
127
136
  const map = new Map();
128
137
  for (const o of raw)
129
138
  map.set(o.reactorId, o);
130
139
  return map;
131
- }, [outcomesMap, flowCorrelationId]);
140
+ }, [outcomesMap, flowWorkflowId]);
132
141
  const treeEvents = useMemo(() => {
133
142
  const all = causalTree?.events ?? null;
134
143
  if (all == null || (scrubberStart == null && scrubberEnd == null))
@@ -137,47 +146,47 @@ export function CausalTreePane({ onInvestigate } = {}) {
137
146
  }, [causalTree?.events, scrubberStart, scrubberEnd]);
138
147
  const treeLoading = selectedSeq != null && causalTree == null;
139
148
  const onClickReactor = useCallback((reactorId, _parentEventId) => {
140
- if (flowCorrelationId) {
149
+ if (flowWorkflowId) {
141
150
  dispatch({ type: "ui/flow_node_selected", payload: { kind: "reactor", reactorId } });
142
151
  }
143
152
  dispatch({ type: "ui/handler_selected", payload: { reactorId } });
144
- }, [flowCorrelationId, dispatch]);
153
+ }, [flowWorkflowId, dispatch]);
145
154
  const { roots, childrenMap, totalCount, filteredCount } = useMemo(() => {
146
155
  if (!treeEvents || treeEvents.length === 0)
147
156
  return { roots: [], childrenMap: new Map(), totalCount: 0, filteredCount: 0 };
148
157
  const total = treeEvents.length;
149
- const events = (flowCorrelationId && flowSelection)
158
+ const events = (flowWorkflowId && flowSelection)
150
159
  ? treeEvents.filter(e => matchesFlowSelection(e, flowSelection))
151
160
  : treeEvents;
152
161
  const idSet = new Set(events.map(e => e.id).filter(Boolean));
153
162
  const cMap = new Map();
154
163
  const rootList = [];
155
164
  for (const evt of events) {
156
- if (evt.parentId == null || !idSet.has(evt.parentId)) {
165
+ if (evt.causationId == null || !idSet.has(evt.causationId)) {
157
166
  rootList.push(evt);
158
167
  }
159
168
  else {
160
- const siblings = cMap.get(evt.parentId) ?? [];
169
+ const siblings = cMap.get(evt.causationId) ?? [];
161
170
  siblings.push(evt);
162
- cMap.set(evt.parentId, siblings);
171
+ cMap.set(evt.causationId, siblings);
163
172
  }
164
173
  }
165
174
  rootList.sort((a, b) => a.seq - b.seq);
166
175
  const filtered = rootList.length + [...cMap.values()].reduce((s, a) => s + a.length, 0);
167
176
  return { roots: rootList, childrenMap: cMap, totalCount: total, filteredCount: filtered };
168
- }, [treeEvents, flowCorrelationId, flowSelection]);
177
+ }, [treeEvents, flowWorkflowId, flowSelection]);
169
178
  if (treeLoading) {
170
- return (_jsxs("div", { className: "ci-pulse", style: { padding: 12, display: "flex", flexDirection: "column", gap: 6 }, children: [_jsx("div", { className: "ci-muted-bg", style: { height: 12, width: 128, marginBottom: 12 } }), _jsxs("div", { className: "ci-row", style: { gap: 6 }, children: [_jsx("div", { className: "ci-muted-bg", style: { height: 16, width: 48 } }), _jsx("div", { className: "ci-muted-bg", style: { height: 16, width: 144 } }), _jsx("div", { className: "ci-muted-bg", style: { height: 12, width: 96 } })] }), _jsxs("div", { style: { paddingLeft: 24, display: "flex", flexDirection: "column", gap: 6 }, children: [_jsxs("div", { className: "ci-row", style: { gap: 6 }, children: [_jsx("div", { className: "ci-muted-bg", style: { height: 16, width: 56 } }), _jsx("div", { className: "ci-muted-bg", style: { height: 16, width: 176 } }), _jsx("div", { className: "ci-muted-bg", style: { height: 12, width: 96 } })] }), _jsxs("div", { className: "ci-row", style: { gap: 6 }, children: [_jsx("div", { className: "ci-muted-bg", style: { height: 16, width: 40 } }), _jsx("div", { className: "ci-muted-bg", style: { height: 16, width: 128 } }), _jsx("div", { className: "ci-muted-bg", style: { height: 12, width: 96 } })] })] })] }));
179
+ return (_jsxs("div", { className: "p-3 space-y-1.5 animate-pulse", children: [_jsx("div", { className: "h-3 w-32 bg-muted rounded mb-3" }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-4 w-12 bg-muted rounded" }), _jsx("div", { className: "h-4 w-36 bg-muted rounded" }), _jsx("div", { className: "h-3 w-24 bg-muted rounded" })] }), _jsxs("div", { className: "pl-6 space-y-1.5", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-4 w-14 bg-muted rounded" }), _jsx("div", { className: "h-4 w-44 bg-muted rounded" }), _jsx("div", { className: "h-3 w-24 bg-muted rounded" })] }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "h-4 w-10 bg-muted rounded" }), _jsx("div", { className: "h-4 w-32 bg-muted rounded" }), _jsx("div", { className: "h-3 w-24 bg-muted rounded" })] })] })] }));
171
180
  }
172
181
  if (!treeEvents) {
173
- return (_jsx("div", { className: "ci-empty", children: "Select an event to view its causal tree" }));
182
+ return (_jsx("div", { className: "flex items-center justify-center h-full text-xs text-muted-foreground/50 tracking-wide", children: "Select an event to view its causal tree" }));
174
183
  }
175
184
  if (roots.length === 0 && flowSelection) {
176
- return (_jsxs("div", { style: { height: "100%", overflow: "auto", padding: 12 }, children: [_jsxs("div", { className: "ci-flow-filter ci-row", style: { gap: 8, marginBottom: 8 }, children: [_jsx("span", { children: flowSelection.kind === "event-type"
185
+ return (_jsxs("div", { className: "h-full overflow-y-auto p-3", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2 px-2.5 py-1.5 rounded-md bg-indigo-500/8 border border-indigo-500/15 text-xs text-indigo-400", children: [_jsx("span", { children: flowSelection.kind === "event-type"
177
186
  ? flowSelection.name
178
- : `outputs of ${flowSelection.reactorId}` }), _jsx("button", { onClick: () => dispatch({ type: "ui/flow_node_selected", payload: null }), style: { marginLeft: "auto", background: "none", border: "none", cursor: "pointer", color: "inherit", padding: 0, transition: "color 150ms" }, children: _jsx(X, { size: 12 }) })] }), _jsx("div", { className: "ci-empty", style: { height: 128, fontSize: 14 }, children: "No events match the current filter" })] }));
187
+ : `outputs of ${flowSelection.reactorId}` }), _jsx("button", { onClick: () => dispatch({ type: "ui/flow_node_selected", payload: null }), className: "ml-auto hover:text-foreground", children: _jsx(X, { size: 12 }) })] }), _jsx("div", { className: "flex items-center justify-center h-32 text-sm text-muted-foreground", children: "No events match the current filter" })] }));
179
188
  }
180
- return (_jsxs("div", { style: { height: "100%", overflow: "auto", padding: 12 }, children: [flowSelection && (_jsxs("div", { className: "ci-flow-filter ci-row", style: { gap: 8, marginBottom: 8 }, children: [_jsx("span", { children: flowSelection.kind === "event-type"
189
+ return (_jsxs("div", { className: "h-full overflow-y-auto p-3", children: [flowSelection && (_jsxs("div", { className: "flex items-center gap-2 mb-2 px-2.5 py-1.5 rounded-md bg-indigo-500/8 border border-indigo-500/15 text-xs text-indigo-400", children: [_jsx("span", { children: flowSelection.kind === "event-type"
181
190
  ? flowSelection.name
182
- : `outputs of ${flowSelection.reactorId}` }), _jsx("button", { onClick: () => dispatch({ type: "ui/flow_node_selected", payload: null }), style: { marginLeft: "auto", background: "none", border: "none", cursor: "pointer", color: "inherit", padding: 0, transition: "color 150ms" }, children: _jsx(X, { size: 12 }) })] })), _jsxs("h3", { className: "ci-header", style: { marginBottom: 8 }, children: ["Causal Tree (", flowSelection ? `${filteredCount} of ${totalCount}` : totalCount, " events)"] }), roots.map(root => (_jsx(TreeNode, { event: root, childrenMap: childrenMap, depth: 0, onClickReactor: onClickReactor, onInvestigate: onInvestigate, outcomesByReactor: outcomesByReactor }, root.seq)))] }));
191
+ : `outputs of ${flowSelection.reactorId}` }), _jsx("button", { onClick: () => dispatch({ type: "ui/flow_node_selected", payload: null }), className: "ml-auto hover:text-foreground", children: _jsx(X, { size: 12 }) })] })), _jsxs("h3", { className: "text-[10px] font-semibold text-muted-foreground/50 mb-2 uppercase tracking-widest", children: ["Causal Tree (", flowSelection ? `${filteredCount} of ${totalCount}` : totalCount, " events)"] }), roots.map(root => (_jsx(TreeNode, { event: root, childrenMap: childrenMap, depth: 0, onClickReactor: onClickReactor, onInvestigate: onInvestigate, outcomesByReactor: outcomesByReactor }, root.seq)))] }));
183
192
  }