causal-inspector 0.1.1 → 0.1.4

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.
@@ -119,12 +119,24 @@ export const createQueryEngine = (transport) => {
119
119
  console.error("[causal-inspector] fetch logs failed:", e);
120
120
  }
121
121
  };
122
- const fetchCorrelations = async (search) => {
122
+ let correlationCursor = null;
123
+ const fetchCorrelations = async (opts) => {
124
+ const append = opts?.append ?? false;
125
+ const cursor = append ? correlationCursor : undefined;
123
126
  try {
124
- const data = await transport.query(INSPECTOR_CORRELATIONS, { search: search || undefined, limit: 100 });
127
+ const data = await transport.query(INSPECTOR_CORRELATIONS, {
128
+ search: opts?.search || undefined,
129
+ limit: 50,
130
+ cursor: cursor || undefined,
131
+ });
132
+ correlationCursor = data.inspectorCorrelations.nextCursor;
125
133
  dispatch({
126
134
  type: "events/correlations_loaded",
127
- payload: data.inspectorCorrelations,
135
+ payload: {
136
+ correlations: data.inspectorCorrelations.correlations,
137
+ hasMore: data.inspectorCorrelations.nextCursor != null,
138
+ append,
139
+ },
128
140
  });
129
141
  }
130
142
  catch (e) {
@@ -214,16 +226,16 @@ export const createQueryEngine = (transport) => {
214
226
  fetchCausalTree(event.payload.seq);
215
227
  break;
216
228
  case "ui/filter_changed":
217
- dispatch({
218
- type: "events/page_loaded",
219
- payload: { events: [], hasMore: true },
220
- });
221
229
  fetchEvents();
222
230
  break;
231
+ case "ui/load_more_correlations_requested":
232
+ fetchCorrelations({ append: true });
233
+ break;
223
234
  case "ui/correlations_requested":
224
- fetchCorrelations(event.payload.search);
235
+ correlationCursor = null;
236
+ fetchCorrelations({ search: event.payload.search });
225
237
  stopCorrelationPolling();
226
- correlationPollTimer = setInterval(() => fetchCorrelations(event.payload.search), 5000);
238
+ correlationPollTimer = setInterval(() => fetchCorrelations({ search: event.payload.search }), 5000);
227
239
  break;
228
240
  case "ui/aggregate_lifecycle_requested":
229
241
  fetchAggregateLifecycle(event.payload.aggregateKey);
@@ -18,7 +18,7 @@ function buildSearch(correlationId, handler) {
18
18
  else
19
19
  params.delete("handler");
20
20
  const search = params.toString();
21
- return search ? `?${search}` : window.location.pathname;
21
+ return search ? `${window.location.pathname}?${search}` : window.location.pathname;
22
22
  }
23
23
  /**
24
24
  * URL engine — keeps the browser URL in sync with navigation state.
@@ -51,6 +51,13 @@ export const createUrlEngine = (dispatch, _getState) => {
51
51
  case "ui/flow_closed":
52
52
  window.history.pushState(null, "", buildSearch(null, null));
53
53
  break;
54
+ case "ui/filter_changed": {
55
+ const payload = event.payload;
56
+ if (payload.correlationId !== undefined) {
57
+ window.history.pushState(null, "", buildSearch(payload.correlationId, null));
58
+ }
59
+ break;
60
+ }
54
61
  case "ui/handler_selected":
55
62
  // Replace rather than push — handler changes within a flow are fine as one history entry
56
63
  window.history.replaceState(null, "", buildSearch(new URLSearchParams(window.location.search).get("correlation"), event.payload.reactorId));
package/dist/events.d.ts CHANGED
@@ -31,7 +31,11 @@ type OutcomesLoaded = BaseEvent<"events/outcomes_loaded", {
31
31
  correlationId: string;
32
32
  outcomes: ReactorOutcome[];
33
33
  }>;
34
- type CorrelationsLoaded = BaseEvent<"events/correlations_loaded", CorrelationSummary[]>;
34
+ type CorrelationsLoaded = BaseEvent<"events/correlations_loaded", {
35
+ correlations: CorrelationSummary[];
36
+ hasMore: boolean;
37
+ append: boolean;
38
+ }>;
35
39
  type ReactorDependenciesLoaded = BaseEvent<"events/reactor_dependencies_loaded", ReactorDependency[]>;
36
40
  type AggregateKeysLoaded = BaseEvent<"events/aggregate_keys_loaded", string[]>;
37
41
  type AggregateLifecycleLoaded = BaseEvent<"events/aggregate_lifecycle_loaded", {
@@ -69,9 +73,10 @@ type HandlerSelected = BaseEvent<"ui/handler_selected", {
69
73
  type AggregateLifecycleRequested = BaseEvent<"ui/aggregate_lifecycle_requested", {
70
74
  aggregateKey: string;
71
75
  }>;
76
+ type LoadMoreCorrelationsRequested = BaseEvent<"ui/load_more_correlations_requested">;
72
77
  type LocationChanged = BaseEvent<"location/changed", {
73
78
  correlationId: string | null;
74
79
  handler: string | null;
75
80
  }>;
76
- export type InspectorMachineEvent = EventsReceived | SubscriptionConnected | SubscriptionError | PageLoaded | CausalTreeLoaded | FlowLoaded | LogsLoaded | DescriptionsLoaded | DescriptionSnapshotsLoaded | AggregateTimelineLoaded | OutcomesLoaded | CorrelationsLoaded | EventSelected | EventDeselected | FlowOpened | FlowClosed | FlowNodeSelected | FilterChanged | LoadMoreRequested | LayoutChanged | ScrubberStartChanged | ScrubberEndChanged | ScrubberPlayToggled | ScrubberSpeedChanged | CorrelationsRequested | ReactorDependenciesLoaded | AggregateKeysLoaded | HandlerSelected | LocationChanged | AggregateLifecycleLoaded | AggregateLifecycleRequested;
81
+ export type InspectorMachineEvent = EventsReceived | SubscriptionConnected | SubscriptionError | PageLoaded | CausalTreeLoaded | FlowLoaded | LogsLoaded | DescriptionsLoaded | DescriptionSnapshotsLoaded | AggregateTimelineLoaded | OutcomesLoaded | CorrelationsLoaded | EventSelected | EventDeselected | FlowOpened | FlowClosed | FlowNodeSelected | FilterChanged | LoadMoreRequested | LayoutChanged | ScrubberStartChanged | ScrubberEndChanged | ScrubberPlayToggled | ScrubberSpeedChanged | CorrelationsRequested | ReactorDependenciesLoaded | AggregateKeysLoaded | HandlerSelected | LocationChanged | AggregateLifecycleLoaded | AggregateLifecycleRequested | LoadMoreCorrelationsRequested;
77
82
  export {};
@@ -5,58 +5,23 @@ import { FilterBar } from "../components/FilterBar";
5
5
  import { CopyablePayload } from "../components/CopyablePayload";
6
6
  import { eventTextColor, eventBg } from "../theme";
7
7
  import { formatTs, compactPayload, aggregateKey, inScrubberRange } from "../utils";
8
- import { Search, ChevronRight, ChevronDown } from "lucide-react";
9
- function EventRow({ event, isSelected, onClick, onFilterCorrelation, onFilterStream, onInvestigate, indent, }) {
8
+ import { Search, ChevronRight } from "lucide-react";
9
+ function EventRow({ event, isSelected, onClick, onFilterCorrelation, onFilterStream, onInvestigate, }) {
10
10
  const [payloadOpen, setPayloadOpen] = useState(false);
11
11
  return (_jsxs("div", { className: `group w-full text-left px-3 py-2 border-b border-border transition-all duration-150 ${isSelected
12
12
  ? "bg-indigo-500/15"
13
- : "hover:bg-white/[0.02]"}`, style: indent ? { paddingLeft: 28 } : undefined, children: [_jsx("div", { onClick: onClick, role: "button", tabIndex: 0, className: "w-full text-left cursor-pointer", children: _jsxs("div", { className: "flex items-center gap-2.5 min-w-0", children: [_jsx("span", { className: "text-[10px] font-mono text-muted-foreground/60 w-10 shrink-0 text-right tabular-nums", children: event.seq }), _jsx("span", { className: "text-[10px] text-muted-foreground/70 shrink-0 w-32 tabular-nums", children: formatTs(event.ts) }), event.correlationId && (_jsx("button", { onClick: (e) => { e.stopPropagation(); onFilterCorrelation(event.correlationId); }, className: "px-1.5 py-0.5 rounded-full text-[9px] font-mono bg-purple-500/8 text-purple-400/80 hover:bg-purple-500/15 hover:text-purple-400 shrink-0 transition-all border border-purple-500/10", title: `Filter by correlation ${event.correlationId}`, children: event.correlationId.slice(0, 8) })), event.aggregateType && event.aggregateId && (_jsxs("button", { onClick: (e) => { e.stopPropagation(); onFilterStream(aggregateKey(event)); }, className: "opacity-0 group-hover:opacity-100 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: `Filter by stream ${event.aggregateType}:${event.aggregateId}`, children: [event.aggregateType, ":", event.aggregateId.slice(0, 8)] })), _jsx("span", { className: "text-xs font-mono shrink-0 px-1.5 py-0.5 rounded", style: {
13
+ : "hover:bg-white/[0.02]"}`, children: [_jsx("div", { onClick: onClick, role: "button", tabIndex: 0, className: "w-full text-left cursor-pointer", children: _jsxs("div", { className: "flex items-center gap-2.5 min-w-0", children: [_jsx("span", { className: "text-[10px] font-mono text-muted-foreground/60 w-10 shrink-0 text-right tabular-nums", children: event.seq }), _jsx("span", { className: "text-[10px] text-muted-foreground/70 shrink-0 w-32 tabular-nums", children: formatTs(event.ts) }), event.correlationId && (_jsx("button", { onClick: (e) => { e.stopPropagation(); onFilterCorrelation(event.correlationId); }, className: "px-1.5 py-0.5 rounded-full text-[9px] font-mono bg-purple-500/8 text-purple-400/80 hover:bg-purple-500/15 hover:text-purple-400 shrink-0 transition-all border border-purple-500/10", title: `Filter by correlation ${event.correlationId}`, children: event.correlationId.slice(0, 8) })), event.aggregateType && event.aggregateId && (_jsxs("button", { onClick: (e) => { e.stopPropagation(); onFilterStream(aggregateKey(event)); }, className: "opacity-0 group-hover:opacity-100 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: `Filter by stream ${event.aggregateType}:${event.aggregateId}`, children: [event.aggregateType, ":", event.aggregateId.slice(0, 8)] })), _jsx("span", { className: "text-xs font-mono shrink-0 px-1.5 py-0.5 rounded", style: {
14
14
  color: eventTextColor(event.name),
15
15
  background: eventBg(event.name),
16
16
  }, children: event.name }), _jsxs("button", { onClick: (e) => { e.stopPropagation(); setPayloadOpen((v) => !v); }, className: "flex items-center gap-1 text-[10px] font-mono text-muted-foreground/60 hover:text-muted-foreground truncate text-left min-w-0 transition-colors", title: "Click to expand payload", children: [_jsx(ChevronRight, { size: 10, className: `shrink-0 transition-transform duration-150 ${payloadOpen ? "rotate-90" : ""}` }), _jsx("span", { className: "truncate", children: event.summary ?? compactPayload(event.payload) })] }), onInvestigate && (_jsx("button", { onClick: (e) => { e.stopPropagation(); onInvestigate(); }, className: "opacity-0 group-hover:opacity-100 transition-opacity duration-150 ml-auto p-1 rounded-md hover:bg-white/[0.05] shrink-0 text-muted-foreground", title: "Investigate", children: _jsx(Search, { size: 12 }) }))] }) }), payloadOpen && (_jsx(CopyablePayload, { payload: event.payload, className: "mt-2 ml-12 max-h-64" }))] }));
17
17
  }
18
- function groupEventsByCorrelation(events) {
19
- const result = [];
20
- let i = 0;
21
- while (i < events.length) {
22
- const event = events[i];
23
- if (!event.correlationId) {
24
- result.push(event);
25
- i++;
26
- continue;
27
- }
28
- // Collect consecutive events with the same correlationId
29
- const cid = event.correlationId;
30
- const groupEvents = [event];
31
- let j = i + 1;
32
- while (j < events.length && events[j].correlationId === cid) {
33
- groupEvents.push(events[j]);
34
- j++;
35
- }
36
- if (groupEvents.length === 1) {
37
- // Single event — render flat
38
- result.push(event);
39
- }
40
- else {
41
- // Find the root event (no reactorId) or fall back to first
42
- const rootIdx = groupEvents.findIndex((e) => !e.reactorId);
43
- const root = rootIdx >= 0 ? groupEvents[rootIdx] : groupEvents[0];
44
- const children = groupEvents.filter((e) => e !== root);
45
- result.push({ correlationId: cid, root, children });
46
- }
47
- i = j;
48
- }
49
- return result;
50
- }
51
- function EventGroupRow({ group, selectedSeq, collapsed, onToggle, onSelect, onFilterCorrelation, onFilterStream, onInvestigate, }) {
52
- return (_jsxs("div", { children: [_jsxs("div", { className: "relative", children: [_jsx("button", { onClick: onToggle, className: "absolute left-1 top-2.5 z-10 p-0.5 rounded hover:bg-white/[0.05] text-muted-foreground/60 hover:text-muted-foreground transition-colors", title: collapsed ? "Expand group" : "Collapse group", children: collapsed ? _jsx(ChevronRight, { size: 12 }) : _jsx(ChevronDown, { size: 12 }) }), _jsxs("div", { className: "relative", children: [_jsx(EventRow, { event: group.root, isSelected: group.root.seq === selectedSeq, onClick: () => onSelect(group.root), onFilterCorrelation: onFilterCorrelation, onFilterStream: onFilterStream, onInvestigate: onInvestigate ? () => onInvestigate(group.root) : undefined }), collapsed && (_jsxs("span", { className: "absolute right-3 top-2.5 text-[9px] font-mono px-1.5 py-0.5 rounded-full bg-white/[0.04] text-muted-foreground/60 border border-border", children: ["+", group.children.length] }))] })] }), !collapsed &&
53
- group.children.map((child) => (_jsx(EventRow, { event: child, isSelected: child.seq === selectedSeq, onClick: () => onSelect(child), onFilterCorrelation: onFilterCorrelation, onFilterStream: onFilterStream, onInvestigate: onInvestigate ? () => onInvestigate(child) : undefined, indent: true }, child.seq)))] }));
54
- }
55
18
  function InfiniteScrollSentinel({ onVisible, loading }) {
56
19
  const ref = useRef(null);
57
20
  const onVisibleRef = useRef(onVisible);
58
21
  onVisibleRef.current = onVisible;
59
22
  useEffect(() => {
23
+ if (loading)
24
+ return;
60
25
  const el = ref.current;
61
26
  if (!el)
62
27
  return;
@@ -64,35 +29,25 @@ function InfiniteScrollSentinel({ onVisible, loading }) {
64
29
  onVisibleRef.current(); }, { rootMargin: "200px" });
65
30
  observer.observe(el);
66
31
  return () => observer.disconnect();
67
- }, []);
32
+ }, [loading]);
68
33
  return (_jsx("div", { ref: ref, className: "flex items-center justify-center py-4", children: loading && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-indigo-500/50 animate-pulse" }), _jsx("span", { className: "text-[10px] text-muted-foreground/60", children: "Loading" })] })) }));
69
34
  }
70
35
  export function TimelinePane({ onInvestigate } = {}) {
71
- const events = useSelector((s) => s.events);
36
+ const events = useSelector((s) => {
37
+ const cid = s.filters.correlationId;
38
+ return cid ? s.events.filter((e) => e.correlationId === cid) : s.events;
39
+ });
72
40
  const loading = useSelector((s) => s.loading);
73
41
  const hasMore = useSelector((s) => s.hasMore);
74
42
  const selectedSeq = useSelector((s) => s.selectedSeq);
75
43
  const scrubberStart = useSelector((s) => s.scrubberStart);
76
44
  const scrubberEnd = useSelector((s) => s.scrubberEnd);
77
45
  const dispatch = useDispatch();
78
- // Default collapsed — this set tracks which groups are *expanded*
79
- const [expandedGroups, setExpandedGroups] = useState(() => new Set());
80
46
  const displayedEvents = useMemo(() => {
81
47
  if (scrubberStart == null && scrubberEnd == null)
82
48
  return events;
83
49
  return events.filter(e => inScrubberRange(e.seq, scrubberStart, scrubberEnd));
84
50
  }, [events, scrubberStart, scrubberEnd]);
85
- const grouped = useMemo(() => groupEventsByCorrelation(displayedEvents), [displayedEvents]);
86
- const toggleGroup = useCallback((correlationId) => {
87
- setExpandedGroups((prev) => {
88
- const next = new Set(prev);
89
- if (next.has(correlationId))
90
- next.delete(correlationId);
91
- else
92
- next.add(correlationId);
93
- return next;
94
- });
95
- }, []);
96
51
  const handleSelect = useCallback((event) => {
97
52
  dispatch({ type: "ui/event_selected", payload: { seq: event.seq } });
98
53
  if (event.correlationId) {
@@ -108,14 +63,5 @@ export function TimelinePane({ onInvestigate } = {}) {
108
63
  const handleLoadMore = useCallback(() => {
109
64
  dispatch({ type: "ui/load_more_requested" });
110
65
  }, [dispatch]);
111
- return (_jsxs("div", { className: "flex flex-col h-full", children: [_jsx(FilterBar, {}), loading && events.length === 0 ? (_jsx("div", { className: "animate-pulse p-1", children: Array.from({ length: 12 }).map((_, i) => (_jsxs("div", { className: "flex items-center gap-2 px-3 py-2.5 border-b border-border", children: [_jsx("div", { className: "h-3 w-10 bg-white/[0.03] rounded shrink-0" }), _jsx("div", { className: "h-3 w-32 bg-white/[0.03] rounded shrink-0" }), _jsx("div", { className: "h-3 bg-white/[0.03] rounded flex-1", style: { maxWidth: `${150 + (i * 37) % 200}px` } })] }, i))) })) : events.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-32 text-sm text-muted-foreground/60", children: "No events found" })) : (_jsxs("div", { className: "flex-1 overflow-y-auto", children: [grouped.map((item) => {
112
- if ("correlationId" in item && "root" in item) {
113
- // EventGroup
114
- const group = item;
115
- return (_jsx(EventGroupRow, { group: group, selectedSeq: selectedSeq, collapsed: !expandedGroups.has(group.correlationId), onToggle: () => toggleGroup(group.correlationId), onSelect: handleSelect, onFilterCorrelation: handleFilterCorrelation, onFilterStream: handleFilterStream, onInvestigate: onInvestigate }, `group-${group.correlationId}-${group.root.seq}`));
116
- }
117
- // Single event
118
- const event = item;
119
- return (_jsx(EventRow, { event: event, isSelected: event.seq === selectedSeq, onClick: () => handleSelect(event), onFilterCorrelation: handleFilterCorrelation, onFilterStream: handleFilterStream, onInvestigate: onInvestigate ? () => onInvestigate(event) : undefined }, event.seq));
120
- }), hasMore && (_jsx(InfiniteScrollSentinel, { onVisible: handleLoadMore, loading: loading }))] }))] }));
66
+ return (_jsxs("div", { className: "flex flex-col h-full", children: [_jsx(FilterBar, {}), loading && events.length === 0 ? (_jsx("div", { className: "animate-pulse p-1", children: Array.from({ length: 12 }).map((_, i) => (_jsxs("div", { className: "flex items-center gap-2 px-3 py-2.5 border-b border-border", children: [_jsx("div", { className: "h-3 w-10 bg-white/[0.03] rounded shrink-0" }), _jsx("div", { className: "h-3 w-32 bg-white/[0.03] rounded shrink-0" }), _jsx("div", { className: "h-3 bg-white/[0.03] rounded flex-1", style: { maxWidth: `${150 + (i * 37) % 200}px` } })] }, i))) })) : events.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-32 text-sm text-muted-foreground/60", children: "No events found" })) : (_jsxs("div", { className: "flex-1 overflow-y-auto", children: [displayedEvents.map((event) => (_jsx(EventRow, { event: event, isSelected: event.seq === selectedSeq, onClick: () => handleSelect(event), onFilterCorrelation: handleFilterCorrelation, onFilterStream: handleFilterStream, onInvestigate: onInvestigate ? () => onInvestigate(event) : undefined }, event.seq))), hasMore && (_jsx(InfiniteScrollSentinel, { onVisible: handleLoadMore, loading: loading }))] }))] }));
121
67
  }
package/dist/queries.d.ts CHANGED
@@ -11,5 +11,5 @@ export declare const INSPECTOR_AGGREGATE_TIMELINE = "\n query InspectorAggregat
11
11
  export declare const INSPECTOR_REACTOR_DEPENDENCIES = "\n query InspectorReactorDependencies {\n inspectorReactorDependencies {\n reactorId\n inputEventTypes\n outputEventTypes\n }\n }\n";
12
12
  export declare const INSPECTOR_AGGREGATE_KEYS = "\n query InspectorAggregateKeys {\n inspectorAggregateKeys\n }\n";
13
13
  export declare const INSPECTOR_AGGREGATE_LIFECYCLE = "\n query InspectorAggregateLifecycle($aggregateKey: String!, $limit: Int) {\n inspectorAggregateLifecycle(aggregateKey: $aggregateKey, limit: $limit) {\n seq\n eventId\n eventType\n ts\n correlationId\n aggregateKey\n state\n }\n }\n";
14
- export declare const INSPECTOR_CORRELATIONS = "\n query InspectorCorrelations($search: String, $limit: Int) {\n inspectorCorrelations(search: $search, limit: $limit) {\n correlationId\n eventCount\n firstTs\n lastTs\n rootEventType\n hasErrors\n }\n }\n";
14
+ export declare const INSPECTOR_CORRELATIONS = "\n query InspectorCorrelations($search: String, $limit: Int, $cursor: String) {\n inspectorCorrelations(search: $search, limit: $limit, cursor: $cursor) {\n correlations {\n correlationId\n eventCount\n firstTs\n lastTs\n rootEventType\n hasErrors\n }\n nextCursor\n }\n }\n";
15
15
  export declare const INSPECTOR_REACTOR_OUTCOMES = "\n query InspectorReactorOutcomes($correlationId: String!) {\n inspectorReactorOutcomes(correlationId: $correlationId) {\n reactorId\n status\n error\n attempts\n startedAt\n completedAt\n triggeringEventIds\n }\n }\n";
package/dist/queries.js CHANGED
@@ -145,14 +145,17 @@ export const INSPECTOR_AGGREGATE_LIFECYCLE = `
145
145
  }
146
146
  `;
147
147
  export const INSPECTOR_CORRELATIONS = `
148
- query InspectorCorrelations($search: String, $limit: Int) {
149
- inspectorCorrelations(search: $search, limit: $limit) {
150
- correlationId
151
- eventCount
152
- firstTs
153
- lastTs
154
- rootEventType
155
- hasErrors
148
+ query InspectorCorrelations($search: String, $limit: Int, $cursor: String) {
149
+ inspectorCorrelations(search: $search, limit: $limit, cursor: $cursor) {
150
+ correlations {
151
+ correlationId
152
+ eventCount
153
+ firstTs
154
+ lastTs
155
+ rootEventType
156
+ hasErrors
157
+ }
158
+ nextCursor
156
159
  }
157
160
  }
158
161
  `;
package/dist/reducer.js CHANGED
@@ -47,8 +47,6 @@ export const reducer = (draft, event) => {
47
47
  // ── Subscription ──
48
48
  case "events/received": {
49
49
  const newEvents = event.payload;
50
- // Filter subscription events against active filters so they don't
51
- // pollute the view when the user has a search or correlation filter.
52
50
  const filtered = newEvents.filter((e) => {
53
51
  if (draft.filters.correlationId && e.correlationId !== draft.filters.correlationId) {
54
52
  return false;
@@ -111,10 +109,18 @@ export const reducer = (draft, event) => {
111
109
  draft.outcomes[correlationId] = outcomes;
112
110
  break;
113
111
  }
114
- case "events/correlations_loaded":
115
- draft.correlations = event.payload;
112
+ case "events/correlations_loaded": {
113
+ const { correlations, hasMore, append } = event.payload;
114
+ if (append) {
115
+ draft.correlations.push(...correlations);
116
+ }
117
+ else {
118
+ draft.correlations = correlations;
119
+ }
120
+ draft.correlationsHasMore = hasMore;
116
121
  draft.correlationsLoading = false;
117
122
  break;
123
+ }
118
124
  case "events/reactor_dependencies_loaded":
119
125
  draft.reactorDependencies = event.payload;
120
126
  break;
@@ -137,6 +143,7 @@ export const reducer = (draft, event) => {
137
143
  break;
138
144
  case "location/changed":
139
145
  applyNavigation(draft, event.payload.correlationId, event.payload.handler);
146
+ draft.filters.correlationId = event.payload.correlationId;
140
147
  break;
141
148
  // ── UI ──
142
149
  case "ui/event_selected":
@@ -181,5 +188,8 @@ export const reducer = (draft, event) => {
181
188
  case "ui/correlations_requested":
182
189
  draft.correlationsLoading = true;
183
190
  break;
191
+ case "ui/load_more_correlations_requested":
192
+ draft.correlationsLoading = true;
193
+ break;
184
194
  }
185
195
  };
package/dist/state.d.ts CHANGED
@@ -24,6 +24,7 @@ export type InspectorState = {
24
24
  outcomes: Record<string, ReactorOutcome[]>;
25
25
  correlations: CorrelationSummary[];
26
26
  correlationsLoading: boolean;
27
+ correlationsHasMore: boolean;
27
28
  reactorDependencies: ReactorDependency[];
28
29
  aggregateKeys: string[];
29
30
  aggregateLifecycle: AggregateLifecycleEntry[];
package/dist/state.js CHANGED
@@ -28,6 +28,7 @@ export const initialState = {
28
28
  outcomes: {},
29
29
  correlations: [],
30
30
  correlationsLoading: false,
31
+ correlationsHasMore: true,
31
32
  reactorDependencies: [],
32
33
  aggregateKeys: [],
33
34
  aggregateLifecycle: [],
package/dist/types.d.ts CHANGED
@@ -113,6 +113,10 @@ export type CorrelationSummary = {
113
113
  rootEventType: string;
114
114
  hasErrors: boolean;
115
115
  };
116
+ export type CorrelationSummaryPage = {
117
+ correlations: CorrelationSummary[];
118
+ nextCursor: string | null;
119
+ };
116
120
  export type FilterState = {
117
121
  search: string;
118
122
  correlationId: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "causal-inspector",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",