causal-inspector 0.1.4 → 0.1.6

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.
@@ -4,16 +4,18 @@ import { useSelector, useDispatch } from "../machine";
4
4
  import { FilterBar } from "../components/FilterBar";
5
5
  import { CopyablePayload } from "../components/CopyablePayload";
6
6
  import { eventTextColor, eventBg } from "../theme";
7
- import { formatTs, compactPayload, aggregateKey, inScrubberRange } from "../utils";
7
+ import { formatTs, compactPayload, inScrubberRange } from "../utils";
8
8
  import { Search, ChevronRight } from "lucide-react";
9
- function EventRow({ event, isSelected, onClick, onFilterCorrelation, onFilterStream, onInvestigate, }) {
9
+ function EventRow({ event, isSelected, onClick, onFilterCorrelation, onInvestigate, }) {
10
10
  const [payloadOpen, setPayloadOpen] = useState(false);
11
- return (_jsxs("div", { className: `group w-full text-left px-3 py-2 border-b border-border transition-all duration-150 ${isSelected
12
- ? "bg-indigo-500/15"
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: {
11
+ return (_jsxs("div", { className: `ci-event-row${isSelected ? " ci-selected" : ""}`, children: [_jsx("div", { onClick: onClick, role: "button", tabIndex: 0, style: { width: "100%", textAlign: "left", cursor: "pointer" }, children: _jsxs("div", { className: "ci-row", style: { gap: 10, minWidth: 0 }, children: [_jsx("span", { className: "ci-mono ci-tabular", style: { fontSize: 10, color: "var(--ci-text-dim)", width: 40, flexShrink: 0, textAlign: "right" }, children: event.seq }), _jsx("span", { className: "ci-tabular", style: { fontSize: 10, color: "var(--ci-text-muted)", opacity: 0.7, flexShrink: 0, width: 128 }, children: formatTs(event.ts) }), event.correlationId && (_jsx("button", { onClick: (e) => { e.stopPropagation(); onFilterCorrelation(event.correlationId); }, className: "ci-corr-pill", title: `Filter by correlation ${event.correlationId}`, style: { flexShrink: 0 }, children: event.correlationId.slice(0, 8) })), _jsx("span", { className: "ci-mono", style: {
12
+ fontSize: 12,
13
+ flexShrink: 0,
14
+ padding: "2px 6px",
15
+ borderRadius: "var(--ci-radius-sm)",
14
16
  color: eventTextColor(event.name),
15
17
  background: eventBg(event.name),
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" }))] }));
18
+ }, children: event.name }), _jsxs("button", { onClick: (e) => { e.stopPropagation(); setPayloadOpen((v) => !v); }, className: "ci-mono ci-truncate", style: { display: "flex", alignItems: "center", gap: 4, fontSize: 10, color: "var(--ci-text-dim)", textAlign: "left", minWidth: 0, background: "none", border: "none", cursor: "pointer", transition: "color 150ms" }, title: "Click to expand payload", children: [_jsx(ChevronRight, { size: 10, style: { flexShrink: 0, transition: "transform 150ms", transform: payloadOpen ? "rotate(90deg)" : undefined } }), _jsx("span", { className: "ci-truncate", children: event.summary ?? compactPayload(event.payload) })] }), onInvestigate && (_jsx("button", { onClick: (e) => { e.stopPropagation(); onInvestigate(); }, className: "ci-copy-btn ci-btn", style: { marginLeft: "auto", flexShrink: 0 }, title: "Investigate", children: _jsx(Search, { size: 12 }) }))] }) }), payloadOpen && (_jsx("div", { style: { marginTop: 8, marginLeft: 48, maxHeight: 256 }, children: _jsx(CopyablePayload, { payload: event.payload }) }))] }));
17
19
  }
18
20
  function InfiniteScrollSentinel({ onVisible, loading }) {
19
21
  const ref = useRef(null);
@@ -30,12 +32,21 @@ function InfiniteScrollSentinel({ onVisible, loading }) {
30
32
  observer.observe(el);
31
33
  return () => observer.disconnect();
32
34
  }, [loading]);
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" })] })) }));
35
+ return (_jsx("div", { ref: ref, className: "ci-row", style: { justifyContent: "center", padding: "16px 0" }, children: loading && (_jsxs("div", { className: "ci-row", style: { gap: 8 }, children: [_jsx("div", { className: "ci-pulse", style: { width: 6, height: 6, borderRadius: "50%", background: "rgba(99, 102, 241, 0.5)" } }), _jsx("span", { style: { fontSize: 10, color: "var(--ci-text-dim)" }, children: "Loading" })] })) }));
34
36
  }
35
37
  export function TimelinePane({ onInvestigate } = {}) {
36
38
  const events = useSelector((s) => {
39
+ let result = s.events;
37
40
  const cid = s.filters.correlationId;
38
- return cid ? s.events.filter((e) => e.correlationId === cid) : s.events;
41
+ if (cid)
42
+ result = result.filter((e) => e.correlationId === cid);
43
+ const search = s.filters.search?.toLowerCase();
44
+ if (search) {
45
+ result = result.filter((e) => e.name.toLowerCase().includes(search) ||
46
+ e.payload.toLowerCase().includes(search) ||
47
+ (e.correlationId ?? "").toLowerCase().includes(search));
48
+ }
49
+ return result;
39
50
  });
40
51
  const loading = useSelector((s) => s.loading);
41
52
  const hasMore = useSelector((s) => s.hasMore);
@@ -57,11 +68,8 @@ export function TimelinePane({ onInvestigate } = {}) {
57
68
  const handleFilterCorrelation = useCallback((correlationId) => {
58
69
  dispatch({ type: "ui/filter_changed", payload: { correlationId } });
59
70
  }, [dispatch]);
60
- const handleFilterStream = useCallback((aggregateKey) => {
61
- dispatch({ type: "ui/filter_changed", payload: { aggregateKey } });
62
- }, [dispatch]);
63
71
  const handleLoadMore = useCallback(() => {
64
72
  dispatch({ type: "ui/load_more_requested" });
65
73
  }, [dispatch]);
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 }))] }))] }));
74
+ return (_jsxs("div", { className: "ci-col", children: [_jsx(FilterBar, {}), loading && events.length === 0 ? (_jsx("div", { className: "ci-pulse", style: { padding: 4 }, children: Array.from({ length: 12 }).map((_, i) => (_jsxs("div", { className: "ci-row", style: { gap: 8, padding: "10px 12px", borderBottom: "1px solid var(--ci-border)" }, children: [_jsx("div", { className: "ci-skeleton", style: { height: 12, width: 40, flexShrink: 0 } }), _jsx("div", { className: "ci-skeleton", style: { height: 12, width: 128, flexShrink: 0 } }), _jsx("div", { className: "ci-skeleton", style: { height: 12, flex: 1, maxWidth: `${150 + (i * 37) % 200}px` } })] }, i))) })) : events.length === 0 ? (_jsx("div", { className: "ci-empty", style: { height: 128, fontSize: 14 }, children: "No events found" })) : (_jsxs("div", { className: "ci-scroll", children: [displayedEvents.map((event) => (_jsx(EventRow, { event: event, isSelected: event.seq === selectedSeq, onClick: () => handleSelect(event), onFilterCorrelation: handleFilterCorrelation, onInvestigate: onInvestigate ? () => onInvestigate(event) : undefined }, event.seq))), hasMore && (_jsx(InfiniteScrollSentinel, { onVisible: handleLoadMore, loading: loading }))] }))] }));
67
75
  }
@@ -27,6 +27,7 @@ const STATUS_COLORS = {
27
27
  completed: { bar: "#22c55e", barEnd: "#16a34a", text: "#bbf7d0" },
28
28
  running: { bar: "#eab308", barEnd: "#ca8a04", text: "#fef08a" },
29
29
  error: { bar: "#ef4444", barEnd: "#dc2626", text: "#fecaca" },
30
+ retry: { bar: "#f97316", barEnd: "#ea580c", text: "#fed7aa" },
30
31
  };
31
32
  function statusColor(status) {
32
33
  return STATUS_COLORS[status] ?? STATUS_COLORS.running;
@@ -57,15 +58,48 @@ function buildBars(outcomes) {
57
58
  return { bars, minMs, maxMs };
58
59
  }
59
60
  // ---------------------------------------------------------------------------
61
+ // Attempt helpers
62
+ // ---------------------------------------------------------------------------
63
+ /** Group attempts by reactorId, sorted by attempt number. */
64
+ function groupAttempts(attempts) {
65
+ const map = new Map();
66
+ for (const a of attempts) {
67
+ let list = map.get(a.reactorId);
68
+ if (!list) {
69
+ list = [];
70
+ map.set(a.reactorId, list);
71
+ }
72
+ list.push(a);
73
+ }
74
+ for (const list of map.values()) {
75
+ list.sort((a, b) => a.attempt - b.attempt);
76
+ }
77
+ return map;
78
+ }
79
+ /** Factor attempts into the global min/max time range. */
80
+ function expandRange(attempts, minMs, maxMs) {
81
+ for (const a of attempts) {
82
+ const s = new Date(a.startedAt).getTime();
83
+ const e = new Date(a.completedAt).getTime();
84
+ if (s < minMs)
85
+ minMs = s;
86
+ if (e > maxMs)
87
+ maxMs = e;
88
+ }
89
+ return { minMs, maxMs };
90
+ }
91
+ // ---------------------------------------------------------------------------
60
92
  // Constants
61
93
  // ---------------------------------------------------------------------------
62
94
  const ROW_HEIGHT = 36;
95
+ const ATTEMPT_ROW_HEIGHT = 26;
63
96
  const LABEL_WIDTH = 160;
64
97
  const BAR_MIN_WIDTH = 4;
65
98
  const PADDING_X = 12;
66
99
  export function WaterfallPane() {
67
100
  const correlationId = useSelector((s) => s.flowCorrelationId);
68
101
  const outcomes = useSelector((s) => correlationId ? s.outcomes[correlationId] ?? [] : []);
102
+ const attempts = useSelector((s) => correlationId ? s.attempts[correlationId] ?? [] : []);
69
103
  const flowData = useSelector((s) => s.flowData);
70
104
  const scrubberStart = useSelector((s) => s.scrubberStart);
71
105
  const scrubberEnd = useSelector((s) => s.scrubberEnd);
@@ -74,7 +108,17 @@ export function WaterfallPane() {
74
108
  const handleBarClick = useCallback((bar) => {
75
109
  dispatch({ type: "ui/handler_selected", payload: { reactorId: bar.reactorId } });
76
110
  }, [dispatch]);
77
- const { bars, minMs, maxMs } = useMemo(() => buildBars(outcomes), [outcomes]);
111
+ const attemptsByReactor = useMemo(() => groupAttempts(attempts), [attempts]);
112
+ const { bars, minMs, maxMs } = useMemo(() => {
113
+ const result = buildBars(outcomes);
114
+ // Expand range to include attempt times
115
+ if (attempts.length > 0) {
116
+ const expanded = expandRange(attempts, result.minMs, result.maxMs);
117
+ result.minMs = expanded.minMs;
118
+ result.maxMs = expanded.maxMs;
119
+ }
120
+ return result;
121
+ }, [outcomes, attempts]);
78
122
  const rangeMs = maxMs - minMs || 1;
79
123
  // Map event id → seq for scrubber sync
80
124
  const eventIdToSeq = useMemo(() => {
@@ -129,78 +173,147 @@ export function WaterfallPane() {
129
173
  ? ((scrubberMs - minMs) / rangeMs) * 100
130
174
  : null;
131
175
  const isSelected = logsFilter.reactorId === bar.reactorId;
132
- return (_jsxs("div", { onClick: () => handleBarClick(bar), style: {
133
- display: "flex",
134
- alignItems: "center",
135
- height: ROW_HEIGHT,
136
- gap: 0,
137
- opacity: isFuture ? 0.25 : (hasReactorFilter && !isSelected) ? 0.35 : 1,
138
- transition: "opacity 200ms, background 150ms",
139
- cursor: "pointer",
140
- borderRadius: 6,
141
- background: isSelected ? "rgba(99, 102, 241, 0.15)" : "transparent",
142
- paddingLeft: 4,
143
- paddingRight: 4,
144
- }, children: [_jsx("div", { style: {
145
- width: LABEL_WIDTH,
146
- flexShrink: 0,
147
- fontSize: 11,
148
- color: "#c0c0d0",
149
- fontWeight: 500,
150
- overflow: "hidden",
151
- textOverflow: "ellipsis",
152
- whiteSpace: "nowrap",
153
- paddingRight: 10,
154
- letterSpacing: "0.01em",
155
- }, title: bar.reactorId, children: bar.reactorId }), _jsxs("div", { style: {
156
- flex: 1,
157
- position: "relative",
158
- height: 22,
159
- background: "rgba(255, 255, 255, 0.02)",
160
- borderRadius: 5,
161
- overflow: "hidden",
176
+ const reactorAttempts = attemptsByReactor.get(bar.reactorId) ?? [];
177
+ const hasMultipleAttempts = reactorAttempts.length > 1;
178
+ return (_jsxs("div", { children: [_jsxs("div", { onClick: () => handleBarClick(bar), style: {
179
+ display: "flex",
180
+ alignItems: "center",
181
+ height: ROW_HEIGHT,
182
+ gap: 0,
183
+ opacity: isFuture ? 0.25 : (hasReactorFilter && !isSelected) ? 0.35 : 1,
184
+ transition: "opacity 200ms, background 150ms",
185
+ cursor: "pointer",
186
+ borderRadius: 6,
187
+ background: isSelected ? "rgba(99, 102, 241, 0.15)" : "transparent",
188
+ paddingLeft: 4,
189
+ paddingRight: 4,
162
190
  }, children: [_jsx("div", { style: {
163
- position: "absolute",
164
- left: `${offsetPct}%`,
165
- width: `${widthPct}%`,
166
- minWidth: BAR_MIN_WIDTH,
167
- height: "100%",
168
- background: `linear-gradient(90deg, ${colors.bar}, ${colors.barEnd})`,
169
- borderRadius: 4,
170
- opacity: 0.8,
191
+ width: LABEL_WIDTH,
192
+ flexShrink: 0,
193
+ fontSize: 11,
194
+ color: "#c0c0d0",
195
+ fontWeight: 500,
196
+ overflow: "hidden",
197
+ textOverflow: "ellipsis",
198
+ whiteSpace: "nowrap",
199
+ paddingRight: 10,
200
+ letterSpacing: "0.01em",
201
+ }, title: bar.reactorId, children: bar.reactorId }), _jsxs("div", { style: {
202
+ flex: 1,
203
+ position: "relative",
204
+ height: 22,
205
+ background: "rgba(255, 255, 255, 0.02)",
206
+ borderRadius: 5,
207
+ overflow: "hidden",
208
+ }, children: [_jsx("div", { style: {
209
+ position: "absolute",
210
+ left: `${offsetPct}%`,
211
+ width: `${widthPct}%`,
212
+ minWidth: BAR_MIN_WIDTH,
213
+ height: "100%",
214
+ background: `linear-gradient(90deg, ${colors.bar}, ${colors.barEnd})`,
215
+ borderRadius: 4,
216
+ opacity: 0.8,
217
+ display: "flex",
218
+ alignItems: "center",
219
+ paddingLeft: 5,
220
+ paddingRight: 5,
221
+ boxShadow: `0 1px 4px ${colors.bar}30`,
222
+ }, title: `${bar.reactorId}: ${bar.status} (${formatDuration(duration)})${bar.attempts > 1 ? ` — ${bar.attempts} attempts` : ""}${bar.error ? `\nError: ${bar.error}` : ""}`, children: _jsx("span", { style: {
223
+ fontSize: 9,
224
+ fontWeight: 600,
225
+ color: "#0a0a0f",
226
+ whiteSpace: "nowrap",
227
+ overflow: "hidden",
228
+ textOverflow: "ellipsis",
229
+ }, children: formatDuration(duration) }) }), cursorPct != null && (_jsx("div", { style: {
230
+ position: "absolute",
231
+ left: `${cursorPct}%`,
232
+ top: 0,
233
+ bottom: 0,
234
+ width: 1,
235
+ background: "#6366f1",
236
+ pointerEvents: "none",
237
+ boxShadow: "0 0 4px rgba(99, 102, 241, 0.3)",
238
+ } }))] }), _jsxs("div", { style: {
239
+ width: 80,
240
+ flexShrink: 0,
171
241
  display: "flex",
172
242
  alignItems: "center",
173
- paddingLeft: 5,
174
- paddingRight: 5,
175
- boxShadow: `0 1px 4px ${colors.bar}30`,
176
- }, title: `${bar.reactorId}: ${bar.status} (${formatDuration(duration)})${bar.attempts > 1 ? ` — ${bar.attempts} attempts` : ""}${bar.error ? `\nError: ${bar.error}` : ""}`, children: _jsx("span", { style: {
243
+ gap: 4,
244
+ paddingLeft: 10,
245
+ }, children: [_jsx("span", { style: {
246
+ fontSize: 10,
247
+ fontWeight: 500,
248
+ color: colors.text,
249
+ opacity: 0.8,
250
+ }, children: bar.status }), bar.attempts > 1 && (_jsxs("span", { style: { fontSize: 9, color: "#50506a" }, children: ["x", bar.attempts] }))] })] }), bar.status === "error" && bar.error && (_jsx("div", { style: {
251
+ marginLeft: LABEL_WIDTH + 4,
252
+ marginTop: -2,
253
+ marginBottom: 4,
254
+ fontSize: 10,
255
+ color: "#f87171",
256
+ paddingLeft: 4,
257
+ overflow: "hidden",
258
+ textOverflow: "ellipsis",
259
+ whiteSpace: "nowrap",
260
+ }, title: bar.error, children: bar.error })), hasMultipleAttempts && reactorAttempts.map((att) => {
261
+ const attStartMs = new Date(att.startedAt).getTime();
262
+ const attEndMs = new Date(att.completedAt).getTime();
263
+ const attOffsetPct = ((attStartMs - minMs) / rangeMs) * 100;
264
+ const attWidthPct = Math.max(((attEndMs - attStartMs) / rangeMs) * 100, 0.3);
265
+ const attColors = statusColor(att.status);
266
+ const attDuration = attEndMs - attStartMs;
267
+ return (_jsxs("div", { style: {
268
+ display: "flex",
269
+ alignItems: "center",
270
+ height: ATTEMPT_ROW_HEIGHT,
271
+ opacity: isFuture ? 0.25 : (hasReactorFilter && !isSelected) ? 0.35 : 0.7,
272
+ paddingLeft: 4,
273
+ paddingRight: 4,
274
+ }, children: [_jsxs("div", { style: {
275
+ width: LABEL_WIDTH,
276
+ flexShrink: 0,
177
277
  fontSize: 9,
178
- fontWeight: 600,
179
- color: "#0a0a0f",
180
- whiteSpace: "nowrap",
278
+ color: "#70708a",
279
+ paddingLeft: 16,
181
280
  overflow: "hidden",
182
281
  textOverflow: "ellipsis",
183
- }, children: formatDuration(duration) }) }), cursorPct != null && (_jsx("div", { style: {
184
- position: "absolute",
185
- left: `${cursorPct}%`,
186
- top: 0,
187
- bottom: 0,
188
- width: 1,
189
- background: "#6366f1",
190
- pointerEvents: "none",
191
- boxShadow: "0 0 4px rgba(99, 102, 241, 0.3)",
192
- } }))] }), _jsxs("div", { style: {
193
- width: 80,
194
- flexShrink: 0,
195
- display: "flex",
196
- alignItems: "center",
197
- gap: 4,
198
- paddingLeft: 10,
199
- }, children: [_jsx("span", { style: {
200
- fontSize: 10,
201
- fontWeight: 500,
202
- color: colors.text,
203
- opacity: 0.8,
204
- }, children: bar.status }), bar.attempts > 1 && (_jsxs("span", { style: { fontSize: 9, color: "#50506a" }, children: ["x", bar.attempts] }))] })] }, bar.reactorId));
282
+ whiteSpace: "nowrap",
283
+ paddingRight: 10,
284
+ }, children: ["attempt ", att.attempt + 1] }), _jsx("div", { style: {
285
+ flex: 1,
286
+ position: "relative",
287
+ height: 14,
288
+ borderRadius: 3,
289
+ }, children: _jsx("div", { style: {
290
+ position: "absolute",
291
+ left: `${attOffsetPct}%`,
292
+ width: `${attWidthPct}%`,
293
+ minWidth: 3,
294
+ height: "100%",
295
+ background: `linear-gradient(90deg, ${attColors.bar}, ${attColors.barEnd})`,
296
+ borderRadius: 3,
297
+ opacity: 0.6,
298
+ display: "flex",
299
+ alignItems: "center",
300
+ paddingLeft: 4,
301
+ paddingRight: 4,
302
+ }, title: `Attempt ${att.attempt + 1}: ${att.status} (${formatDuration(attDuration)})${att.error ? `\n${att.error}` : ""}`, children: _jsx("span", { style: {
303
+ fontSize: 8,
304
+ fontWeight: 600,
305
+ color: "#0a0a0f",
306
+ whiteSpace: "nowrap",
307
+ overflow: "hidden",
308
+ textOverflow: "ellipsis",
309
+ }, children: formatDuration(attDuration) }) }) }), _jsxs("div", { style: {
310
+ width: 80,
311
+ flexShrink: 0,
312
+ paddingLeft: 10,
313
+ fontSize: 9,
314
+ color: attColors.text,
315
+ opacity: 0.6,
316
+ }, children: [att.status, att.error && (_jsx("span", { style: { color: "#70708a", marginLeft: 4 }, title: att.error, children: "!" }))] })] }, `${att.reactorId}-attempt-${att.attempt}`));
317
+ })] }, bar.reactorId));
205
318
  }), _jsxs("div", { style: { marginTop: 10, fontSize: 10, color: "#40405a", marginLeft: LABEL_WIDTH, letterSpacing: "0.03em" }, children: ["Total wall time: ", formatDuration(rangeMs)] })] })] }));
206
319
  }
package/dist/queries.d.ts CHANGED
@@ -13,3 +13,4 @@ export declare const INSPECTOR_AGGREGATE_KEYS = "\n query InspectorAggregateKey
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
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";
16
+ export declare const INSPECTOR_REACTOR_ATTEMPTS = "\n query InspectorReactorAttempts($correlationId: String!) {\n inspectorReactorAttempts(correlationId: $correlationId) {\n eventId\n reactorId\n correlationId\n attempt\n status\n error\n startedAt\n completedAt\n }\n }\n";
package/dist/queries.js CHANGED
@@ -172,3 +172,17 @@ export const INSPECTOR_REACTOR_OUTCOMES = `
172
172
  }
173
173
  }
174
174
  `;
175
+ export const INSPECTOR_REACTOR_ATTEMPTS = `
176
+ query InspectorReactorAttempts($correlationId: String!) {
177
+ inspectorReactorAttempts(correlationId: $correlationId) {
178
+ eventId
179
+ reactorId
180
+ correlationId
181
+ attempt
182
+ status
183
+ error
184
+ startedAt
185
+ completedAt
186
+ }
187
+ }
188
+ `;
package/dist/reducer.js CHANGED
@@ -109,6 +109,11 @@ export const reducer = (draft, event) => {
109
109
  draft.outcomes[correlationId] = outcomes;
110
110
  break;
111
111
  }
112
+ case "events/attempts_loaded": {
113
+ const { correlationId, attempts } = event.payload;
114
+ draft.attempts[correlationId] = attempts;
115
+ break;
116
+ }
112
117
  case "events/correlations_loaded": {
113
118
  const { correlations, hasMore, append } = event.payload;
114
119
  if (append) {
package/dist/state.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { InspectorEvent, CorrelationSummary, ReactorDependency, AggregateLifecycleEntry, FilterState, FlowSelection, ReactorDescription, ReactorDescriptionSnapshot, AggregateTimelineEntry, ReactorLog, ReactorOutcome, LogsFilter, PaneLayout } from "./types";
1
+ import type { InspectorEvent, CorrelationSummary, ReactorDependency, AggregateLifecycleEntry, FilterState, FlowSelection, ReactorDescription, ReactorDescriptionSnapshot, AggregateTimelineEntry, ReactorLog, ReactorOutcome, ReactorAttempt, LogsFilter, PaneLayout } from "./types";
2
2
  export type InspectorState = {
3
3
  events: InspectorEvent[];
4
4
  hasMore: boolean;
@@ -22,6 +22,7 @@ export type InspectorState = {
22
22
  descriptionSnapshots: Record<string, ReactorDescriptionSnapshot[]>;
23
23
  aggregateTimeline: Record<string, AggregateTimelineEntry[]>;
24
24
  outcomes: Record<string, ReactorOutcome[]>;
25
+ attempts: Record<string, ReactorAttempt[]>;
25
26
  correlations: CorrelationSummary[];
26
27
  correlationsLoading: boolean;
27
28
  correlationsHasMore: boolean;
package/dist/state.js CHANGED
@@ -15,6 +15,8 @@ export const initialState = {
15
15
  search: "",
16
16
  correlationId: null,
17
17
  aggregateKey: null,
18
+ from: null,
19
+ to: null,
18
20
  },
19
21
  logs: [],
20
22
  logsFilter: {
@@ -26,6 +28,7 @@ export const initialState = {
26
28
  descriptionSnapshots: {},
27
29
  aggregateTimeline: {},
28
30
  outcomes: {},
31
+ attempts: {},
29
32
  correlations: [],
30
33
  correlationsLoading: false,
31
34
  correlationsHasMore: true,
package/dist/theme.js CHANGED
@@ -28,7 +28,8 @@ function hslToRgb(h, s, l) {
28
28
  return [Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255)];
29
29
  }
30
30
  export const LOG_LEVEL_COLORS = {
31
- debug: "bg-zinc-600/20 text-zinc-400",
32
- info: "bg-indigo-500/15 text-indigo-400",
33
- warn: "bg-amber-500/15 text-amber-400",
31
+ debug: "ci-log-debug",
32
+ info: "ci-log-info",
33
+ warn: "ci-log-warn",
34
+ error: "ci-log-error",
34
35
  };
package/dist/types.d.ts CHANGED
@@ -75,6 +75,16 @@ export type ReactorOutcome = {
75
75
  completedAt: string | null;
76
76
  triggeringEventIds: string[];
77
77
  };
78
+ export type ReactorAttempt = {
79
+ eventId: string;
80
+ reactorId: string;
81
+ correlationId: string;
82
+ attempt: number;
83
+ status: string;
84
+ error: string | null;
85
+ startedAt: string;
86
+ completedAt: string;
87
+ };
78
88
  export type ReactorDescriptionSnapshot = {
79
89
  seq: number;
80
90
  eventId: string;
@@ -113,14 +123,12 @@ export type CorrelationSummary = {
113
123
  rootEventType: string;
114
124
  hasErrors: boolean;
115
125
  };
116
- export type CorrelationSummaryPage = {
117
- correlations: CorrelationSummary[];
118
- nextCursor: string | null;
119
- };
120
126
  export type FilterState = {
121
127
  search: string;
122
128
  correlationId: string | null;
123
129
  aggregateKey: string | null;
130
+ from: string | null;
131
+ to: string | null;
124
132
  };
125
133
  export type LogsFilter = {
126
134
  scope: "reactor" | "correlation";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "causal-inspector",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",