@workflow/web-shared 5.0.0-beta.24 → 5.0.0-beta.26

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 (95) hide show
  1. package/README.md +2 -10
  2. package/dist/components/event-list-view.d.ts +30 -0
  3. package/dist/components/event-list-view.d.ts.map +1 -1
  4. package/dist/components/event-list-view.js +27 -12
  5. package/dist/components/index.d.ts +4 -3
  6. package/dist/components/index.d.ts.map +1 -1
  7. package/dist/components/index.js +3 -3
  8. package/dist/components/new-trace-viewer/components/copy-button.js +2 -2
  9. package/dist/components/new-trace-viewer/components/event-list.js +2 -2
  10. package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js +2 -2
  11. package/dist/components/new-trace-viewer/components/span-markers.js +3 -3
  12. package/dist/components/new-trace-viewer/components/split-pane.js +2 -2
  13. package/dist/components/new-trace-viewer/components/timeline.js +2 -2
  14. package/dist/components/new-trace-viewer/trace-viewer.js +4 -4
  15. package/dist/components/run-trace-view.d.ts +5 -7
  16. package/dist/components/run-trace-view.d.ts.map +1 -1
  17. package/dist/components/run-trace-view.js +3 -3
  18. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  19. package/dist/components/sidebar/attribute-panel.js +3 -5
  20. package/dist/components/sidebar/detail-card.js +2 -2
  21. package/dist/components/sidebar/entity-detail-panel.d.ts +6 -10
  22. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  23. package/dist/components/sidebar/entity-detail-panel.js +8 -60
  24. package/dist/components/sidebar/events-list.d.ts +3 -1
  25. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  26. package/dist/components/sidebar/events-list.js +31 -9
  27. package/dist/components/sidebar/sidebar-data-context.d.ts +5 -6
  28. package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -1
  29. package/dist/components/sidebar/sidebar-data-context.js +1 -1
  30. package/dist/components/sidebar/span-detail-merge.d.ts +15 -0
  31. package/dist/components/sidebar/span-detail-merge.d.ts.map +1 -1
  32. package/dist/components/sidebar/span-detail-merge.js +40 -1
  33. package/dist/components/sidebar/use-selected-span-detail.d.ts +16 -0
  34. package/dist/components/sidebar/use-selected-span-detail.d.ts.map +1 -0
  35. package/dist/components/sidebar/use-selected-span-detail.js +93 -0
  36. package/dist/components/ui/alert.js +2 -2
  37. package/dist/components/ui/button.js +2 -2
  38. package/dist/components/ui/card.js +2 -2
  39. package/dist/components/ui/context-card.js +2 -2
  40. package/dist/components/ui/error-card.js +2 -2
  41. package/dist/components/ui/icon-button.js +2 -2
  42. package/dist/components/ui/kbd.js +2 -2
  43. package/dist/components/ui/skeleton.js +2 -2
  44. package/dist/components/ui/tooltip.js +2 -2
  45. package/dist/components/workflow-trace-view.d.ts +6 -7
  46. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  47. package/dist/components/workflow-trace-view.js +3 -6
  48. package/dist/components/workflow-traces/trace-span-construction.d.ts +4 -4
  49. package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
  50. package/dist/components/workflow-traces/trace-span-construction.js +74 -53
  51. package/dist/lib/cn.d.ts +10 -0
  52. package/dist/lib/cn.d.ts.map +1 -0
  53. package/dist/lib/cn.js +83 -0
  54. package/dist/lib/event-materialization.d.ts +0 -3
  55. package/dist/lib/event-materialization.d.ts.map +1 -1
  56. package/dist/lib/event-materialization.js +28 -27
  57. package/dist/lib/trace-builder.d.ts.map +1 -1
  58. package/dist/lib/trace-builder.js +3 -3
  59. package/dist/lib/utils.d.ts +0 -2
  60. package/dist/lib/utils.d.ts.map +1 -1
  61. package/dist/lib/utils.js +1 -6
  62. package/package.json +3 -3
  63. package/src/components/event-list-view.tsx +76 -13
  64. package/src/components/index.ts +10 -6
  65. package/src/components/new-trace-viewer/components/copy-button.tsx +1 -1
  66. package/src/components/new-trace-viewer/components/event-list.tsx +1 -1
  67. package/src/components/new-trace-viewer/components/middle-truncate/middle-truncate.tsx +1 -1
  68. package/src/components/new-trace-viewer/components/span-markers.tsx +2 -2
  69. package/src/components/new-trace-viewer/components/split-pane.tsx +1 -1
  70. package/src/components/new-trace-viewer/components/timeline.tsx +1 -1
  71. package/src/components/new-trace-viewer/trace-viewer.tsx +6 -6
  72. package/src/components/run-trace-view.tsx +10 -14
  73. package/src/components/sidebar/attribute-panel.tsx +2 -4
  74. package/src/components/sidebar/detail-card.tsx +1 -1
  75. package/src/components/sidebar/entity-detail-panel.tsx +23 -94
  76. package/src/components/sidebar/events-list.tsx +58 -8
  77. package/src/components/sidebar/sidebar-data-context.tsx +6 -7
  78. package/src/components/sidebar/span-detail-merge.ts +69 -0
  79. package/src/components/sidebar/use-selected-span-detail.ts +146 -0
  80. package/src/components/ui/alert.tsx +1 -1
  81. package/src/components/ui/button.tsx +1 -1
  82. package/src/components/ui/card.tsx +1 -1
  83. package/src/components/ui/context-card.tsx +1 -1
  84. package/src/components/ui/error-card.tsx +1 -1
  85. package/src/components/ui/icon-button.tsx +1 -1
  86. package/src/components/ui/kbd.tsx +1 -1
  87. package/src/components/ui/skeleton.tsx +1 -1
  88. package/src/components/ui/tooltip.tsx +1 -1
  89. package/src/components/workflow-trace-view.tsx +11 -21
  90. package/src/components/workflow-traces/trace-span-construction.test.ts +85 -0
  91. package/src/components/workflow-traces/trace-span-construction.ts +113 -89
  92. package/src/lib/cn.ts +93 -0
  93. package/src/lib/event-materialization.ts +56 -76
  94. package/src/lib/trace-builder.ts +2 -1
  95. package/src/lib/utils.ts +0 -6
@@ -29,25 +29,14 @@ const MARKER_EVENT_TYPES: Set<Event['eventType']> = new Set([
29
29
  'attr_set',
30
30
  ]);
31
31
 
32
- const findEventByType = (
33
- events: Event[],
34
- eventTypes: Event['eventType'][]
35
- ): Event | undefined => {
36
- for (const eventType of eventTypes) {
37
- const event = events.find((e) => e.eventType === eventType);
38
- if (event) return event;
39
- }
40
- return undefined;
41
- };
32
+ export const getEventTimestamp = (
33
+ event: Event | undefined
34
+ ): Date | undefined => {
35
+ const value = event?.occurredAt ?? event?.createdAt;
36
+ if (!value) return undefined;
42
37
 
43
- const withOccurredAt = <T extends object>(
44
- entity: T,
45
- occurredAt: Event['occurredAt'] | undefined
46
- ): T & { occurredAt?: Date } => {
47
- if (!occurredAt || (entity as { occurredAt?: unknown }).occurredAt != null) {
48
- return entity;
49
- }
50
- return { ...entity, occurredAt };
38
+ const date = value instanceof Date ? value : new Date(value);
39
+ return Number.isNaN(date.getTime()) ? undefined : date;
51
40
  };
52
41
 
53
42
  /**
@@ -56,7 +45,8 @@ const withOccurredAt = <T extends object>(
56
45
  */
57
46
  export function convertEventsToSpanEvents(
58
47
  events: Event[],
59
- filterTypes = true
48
+ filterTypes = true,
49
+ options: { preferOccurredAt?: boolean } = {}
60
50
  ): SpanEvent[] {
61
51
  return events
62
52
  .filter((event) =>
@@ -64,7 +54,11 @@ export function convertEventsToSpanEvents(
64
54
  )
65
55
  .map((event) => ({
66
56
  name: event.eventType,
67
- timestamp: dateToOtelTime(event.createdAt),
57
+ timestamp: dateToOtelTime(
58
+ options.preferOccurredAt
59
+ ? (getEventTimestamp(event) ?? event.createdAt)
60
+ : event.createdAt
61
+ ),
68
62
  attributes: {
69
63
  eventId: event.eventId,
70
64
  correlationId: event.correlationId,
@@ -81,7 +75,6 @@ export const waitEventsToWaitEntity = (
81
75
  waitId: string;
82
76
  runId: string;
83
77
  createdAt: Date;
84
- occurredAt?: Date;
85
78
  resumeAt?: Date;
86
79
  completedAt?: Date;
87
80
  } | null => {
@@ -92,18 +85,15 @@ export const waitEventsToWaitEntity = (
92
85
  const completedEvent = events.find(
93
86
  (event) => event.eventType === 'wait_completed'
94
87
  );
95
- return withOccurredAt(
96
- {
97
- waitId: startEvent.correlationId,
98
- runId: startEvent.runId,
99
- createdAt: startEvent.createdAt,
100
- resumeAt: startEvent.eventData?.resumeAt
101
- ? new Date(startEvent.eventData.resumeAt)
102
- : undefined,
103
- completedAt: completedEvent?.createdAt,
104
- },
105
- startEvent.occurredAt
106
- );
88
+ return {
89
+ waitId: startEvent.correlationId,
90
+ runId: startEvent.runId,
91
+ createdAt: getEventTimestamp(startEvent) ?? startEvent.createdAt,
92
+ resumeAt: startEvent.eventData?.resumeAt
93
+ ? new Date(startEvent.eventData.resumeAt)
94
+ : undefined,
95
+ completedAt: getEventTimestamp(completedEvent),
96
+ };
107
97
  };
108
98
 
109
99
  /**
@@ -166,7 +156,6 @@ export const stepEventsToStepEntity = (
166
156
  attempt: number;
167
157
  createdAt: Date;
168
158
  updatedAt: Date;
169
- occurredAt?: Date;
170
159
  startedAt?: Date;
171
160
  completedAt?: Date;
172
161
  specVersion?: number;
@@ -195,16 +184,16 @@ export const stepEventsToStepEntity = (
195
184
  case 'step_started':
196
185
  status = 'running';
197
186
  attempt += 1;
198
- if (!startedAt) startedAt = e.createdAt;
187
+ if (!startedAt) startedAt = getEventTimestamp(e) ?? e.createdAt;
199
188
  completedAt = undefined;
200
189
  break;
201
190
  case 'step_completed':
202
191
  status = 'completed';
203
- completedAt = e.createdAt;
192
+ completedAt = getEventTimestamp(e) ?? e.createdAt;
204
193
  break;
205
194
  case 'step_failed':
206
195
  status = 'failed';
207
- completedAt = e.createdAt;
196
+ completedAt = getEventTimestamp(e) ?? e.createdAt;
208
197
  break;
209
198
  case 'step_retrying':
210
199
  status = 'pending';
@@ -217,24 +206,22 @@ export const stepEventsToStepEntity = (
217
206
  if (attempt === 0) attempt = 1;
218
207
 
219
208
  const lastEvent = events[events.length - 1];
220
- const occurrenceEvent =
221
- findEventByType(events, ['step_created', 'step_started']) ?? anchorEvent;
222
-
223
- return withOccurredAt(
224
- {
225
- stepId: anchorEvent.correlationId ?? '',
226
- runId: anchorEvent.runId,
227
- stepName: createdEvent?.eventData?.stepName ?? '',
228
- status,
229
- attempt,
230
- createdAt: anchorEvent.createdAt,
231
- updatedAt: lastEvent?.createdAt ?? anchorEvent.createdAt,
232
- startedAt,
233
- completedAt,
234
- specVersion: anchorEvent.specVersion,
235
- },
236
- occurrenceEvent.occurredAt
237
- );
209
+
210
+ return {
211
+ stepId: anchorEvent.correlationId ?? '',
212
+ runId: anchorEvent.runId,
213
+ stepName: createdEvent?.eventData?.stepName ?? '',
214
+ status,
215
+ attempt,
216
+ createdAt: getEventTimestamp(anchorEvent) ?? anchorEvent.createdAt,
217
+ updatedAt:
218
+ getEventTimestamp(lastEvent) ??
219
+ lastEvent?.createdAt ??
220
+ anchorEvent.createdAt,
221
+ startedAt,
222
+ completedAt,
223
+ specVersion: anchorEvent.specVersion,
224
+ };
238
225
  };
239
226
 
240
227
  /**
@@ -253,25 +240,41 @@ export function stepToSpan(stepEvents: Event[], maxEndTime: Date): Span | null {
253
240
  };
254
241
 
255
242
  const resource = 'step';
256
- const endTime = new Date(step.completedAt ?? maxEndTime);
257
243
 
258
244
  // Include ALL correlated events on the span so the sidebar detail view
259
245
  // can display them. The timeline uses the `showVerticalLine` flag to
260
246
  // determine which events appear as markers.
261
- const events = convertEventsToSpanEvents(stepEvents, false);
247
+ const events = convertEventsToSpanEvents(stepEvents, false, {
248
+ preferOccurredAt: true,
249
+ });
262
250
 
263
- // Use createdAt as span start time, with activeStartTime for when execution began
264
- // This allows visualization of the "queued" period before execution
265
- const spanStartTime = new Date(step.createdAt);
251
+ // Use the event occurrence timestamp for step spans when it is available,
252
+ // falling back to the ingest timestamp used by older event streams.
253
+ const spanStartEvent =
254
+ stepEvents.find((event) => event.eventType === 'step_created') ??
255
+ stepEvents[0];
256
+ const spanStartTime =
257
+ getEventTimestamp(spanStartEvent) ?? new Date(step.createdAt);
266
258
  let activeStartTime = step.startedAt ? new Date(step.startedAt) : undefined;
267
259
  const firstStartEvent = stepEvents.find(
268
260
  (event) => event.eventType === 'step_started'
269
261
  );
270
262
  if (firstStartEvent) {
271
- // `step.startedAt` is the server-side creation timestamp, and `event.createdAt` is
272
- // the client-side creation timestamp. For now, to align the event marker with the
273
- // line we show for step.startedAt, we overwrite here to always use client-side time.
274
- activeStartTime = new Date(firstStartEvent.createdAt);
263
+ activeStartTime =
264
+ getEventTimestamp(firstStartEvent) ?? new Date(firstStartEvent.createdAt);
265
+ }
266
+
267
+ let endTime = new Date(maxEndTime);
268
+ if (step.completedAt) {
269
+ const completedEvent = stepEvents
270
+ .slice()
271
+ .reverse()
272
+ .find(
273
+ (event) =>
274
+ event.eventType === 'step_completed' ||
275
+ event.eventType === 'step_failed'
276
+ );
277
+ endTime = getEventTimestamp(completedEvent) ?? new Date(step.completedAt);
275
278
  }
276
279
 
277
280
  return {
@@ -303,7 +306,6 @@ export const hookEventsToHookEntity = (
303
306
  runId: string;
304
307
  token?: string;
305
308
  createdAt: Date;
306
- occurredAt?: Date;
307
309
  receivedCount: number;
308
310
  lastReceivedAt?: Date;
309
311
  disposedAt?: Date;
@@ -321,18 +323,15 @@ export const hookEventsToHookEntity = (
321
323
  (event) => event.eventType === 'hook_disposed'
322
324
  );
323
325
  const lastReceivedEvent = receivedEvents.at(-1);
324
- return withOccurredAt(
325
- {
326
- hookId: createdEvent.correlationId,
327
- runId: createdEvent.runId,
328
- token: createdEvent.eventData?.token,
329
- createdAt: createdEvent.createdAt,
330
- receivedCount: receivedEvents.length,
331
- lastReceivedAt: lastReceivedEvent?.createdAt || undefined,
332
- disposedAt: disposedEvents.at(-1)?.createdAt || undefined,
333
- },
334
- createdEvent.occurredAt
335
- );
326
+ return {
327
+ hookId: createdEvent.correlationId,
328
+ runId: createdEvent.runId,
329
+ token: createdEvent.eventData?.token,
330
+ createdAt: getEventTimestamp(createdEvent) ?? createdEvent.createdAt,
331
+ receivedCount: receivedEvents.length,
332
+ lastReceivedAt: getEventTimestamp(lastReceivedEvent),
333
+ disposedAt: getEventTimestamp(disposedEvents.at(-1)),
334
+ };
336
335
  };
337
336
 
338
337
  /**
@@ -379,25 +378,50 @@ export function runToSpan(
379
378
  ): Span {
380
379
  const now = nowTime ?? new Date();
381
380
 
381
+ // Prefer the event occurrence timestamp when available so the root span
382
+ // lines up with child spans that already use event occurrence time.
383
+ const runCreatedEvent = runEvents.find(
384
+ (event) => event.eventType === 'run_created'
385
+ );
386
+ const runStartedEvent = runEvents.find(
387
+ (event) => event.eventType === 'run_started'
388
+ );
389
+ const terminalEvent = runEvents
390
+ .slice()
391
+ .reverse()
392
+ .find(
393
+ (event) =>
394
+ event.eventType === 'run_completed' ||
395
+ event.eventType === 'run_failed' ||
396
+ event.eventType === 'run_cancelled'
397
+ );
398
+ const spanStartTime =
399
+ getEventTimestamp(runCreatedEvent) ?? new Date(run.createdAt);
400
+ const activeStartTime =
401
+ getEventTimestamp(runStartedEvent) ??
402
+ (run.startedAt ? new Date(run.startedAt) : undefined);
403
+ const completedAt =
404
+ getEventTimestamp(terminalEvent) ?? run.completedAt ?? undefined;
405
+ const endTime = completedAt ?? now;
406
+
382
407
  // Only embed identification fields — not the full object with
383
- // input/output/error which may contain non-cloneable types.
408
+ // input/output/error which may contain non-cloneable types. Lifecycle
409
+ // timestamps are event-derived so detail rows align with the span timeline.
384
410
  const { input: _i, output: _o, error: _e, ...runIdentity } = run;
385
- const occurrenceEvent = findEventByType(runEvents, [
386
- 'run_created',
387
- 'run_started',
388
- ]);
389
411
  const attributes = {
390
412
  resource: 'run' as const,
391
- data: withOccurredAt(runIdentity, occurrenceEvent?.occurredAt),
413
+ data: {
414
+ ...runIdentity,
415
+ createdAt: spanStartTime,
416
+ startedAt: activeStartTime,
417
+ completedAt,
418
+ },
392
419
  };
393
420
 
394
- // Use createdAt as span start time, with activeStartTime for when execution began
395
- const spanStartTime = new Date(run.createdAt);
396
- const activeStartTime = run.startedAt ? new Date(run.startedAt) : undefined;
397
- const endTime = run.completedAt ?? now;
398
-
399
421
  // Convert run-level events to span events
400
- const events = convertEventsToSpanEvents(runEvents, false);
422
+ const events = convertEventsToSpanEvents(runEvents, false, {
423
+ preferOccurredAt: true,
424
+ });
401
425
 
402
426
  return {
403
427
  spanId: String(run.runId),
package/src/lib/cn.ts ADDED
@@ -0,0 +1,93 @@
1
+ import { type ClassValue, clsx } from 'clsx';
2
+ import { extendTailwindMerge } from 'tailwind-merge';
3
+
4
+ const twMergeConfig = {
5
+ extend: {
6
+ classGroups: {
7
+ 'text-heading': [
8
+ {
9
+ 'text-heading': [
10
+ '72',
11
+ '64',
12
+ '56',
13
+ '48',
14
+ '40',
15
+ '32',
16
+ '24',
17
+ '20',
18
+ '16',
19
+ '14',
20
+ ],
21
+ },
22
+ ],
23
+ 'text-button': [
24
+ {
25
+ 'text-button': ['16', '14', '12'],
26
+ },
27
+ ],
28
+ 'text-label': [
29
+ {
30
+ 'text-label': [
31
+ '20',
32
+ '18',
33
+ '16',
34
+ '14',
35
+ '14-mono',
36
+ '13',
37
+ '13-mono',
38
+ '12',
39
+ '12-mono',
40
+ ],
41
+ },
42
+ ],
43
+ 'text-copy': [
44
+ {
45
+ 'text-copy': ['24', '20', '18', '16', '14', '13', '13-mono'],
46
+ },
47
+ ],
48
+ material: [
49
+ {
50
+ material: [
51
+ 'base',
52
+ 'small',
53
+ 'medium',
54
+ 'large',
55
+ 'tooltip',
56
+ 'menu',
57
+ 'modal',
58
+ 'fullscreen',
59
+ ],
60
+ },
61
+ ],
62
+ },
63
+ // Make each category conflict with itself, so e.g. text-heading-32 overrides text-heading-24
64
+ conflictingClassGroups: {
65
+ 'text-heading': ['text-heading'],
66
+ 'text-button': ['text-button'],
67
+ 'text-label': ['text-label'],
68
+ 'text-copy': ['text-copy'],
69
+ material: ['material'],
70
+ },
71
+ },
72
+ } as const;
73
+
74
+ type CustomClassGroupIds =
75
+ | 'text-heading'
76
+ | 'text-button'
77
+ | 'text-label'
78
+ | 'text-copy'
79
+ | 'material';
80
+
81
+ const customTwMerge = extendTailwindMerge<CustomClassGroupIds>(twMergeConfig);
82
+
83
+ /**
84
+ * Merges Tailwind class names and resolves any conflicts using `clsx` with `tailwind-merge`.
85
+ *
86
+ * @param inputs - An array of class names to merge.
87
+ * @returns A string of merged and optimized class names.
88
+ */
89
+ function cn(...classes: ClassValue[]) {
90
+ return customTwMerge(clsx(...classes));
91
+ }
92
+
93
+ export { cn };
@@ -23,7 +23,6 @@ export interface MaterializedStep {
23
23
  status: StepStatus;
24
24
  attempt: number;
25
25
  createdAt: Date;
26
- occurredAt?: Date;
27
26
  startedAt?: Date;
28
27
  completedAt?: Date;
29
28
  updatedAt: Date;
@@ -36,7 +35,6 @@ export interface MaterializedHook {
36
35
  runId: string;
37
36
  token?: string;
38
37
  createdAt: Date;
39
- occurredAt?: Date;
40
38
  receivedCount: number;
41
39
  lastReceivedAt?: Date;
42
40
  disposedAt?: Date;
@@ -49,7 +47,6 @@ export interface MaterializedWait {
49
47
  runId: string;
50
48
  status: 'waiting' | 'completed';
51
49
  createdAt: Date;
52
- occurredAt?: Date;
53
50
  resumeAt?: Date;
54
51
  completedAt?: Date;
55
52
  /** All events for this wait, in insertion order */
@@ -62,16 +59,6 @@ export interface MaterializedEntities {
62
59
  waits: MaterializedWait[];
63
60
  }
64
61
 
65
- const withOccurredAt = <T extends object>(
66
- entity: T,
67
- occurredAt: Event['occurredAt'] | undefined
68
- ): T & { occurredAt?: Date } => {
69
- if (!occurredAt || (entity as { occurredAt?: unknown }).occurredAt != null) {
70
- return entity;
71
- }
72
- return { ...entity, occurredAt };
73
- };
74
-
75
62
  // ---------------------------------------------------------------------------
76
63
  // Helper: group events by correlationId prefix
77
64
  // ---------------------------------------------------------------------------
@@ -95,6 +82,14 @@ function groupByCorrelationId(
95
82
  return groups;
96
83
  }
97
84
 
85
+ function getEventTimestamp(event: Event | undefined): Date | undefined {
86
+ const value = event?.occurredAt ?? event?.createdAt;
87
+ if (!value) return undefined;
88
+
89
+ const date = value instanceof Date ? value : new Date(value);
90
+ return Number.isNaN(date.getTime()) ? undefined : date;
91
+ }
92
+
98
93
  // ---------------------------------------------------------------------------
99
94
  // materializeSteps
100
95
  // ---------------------------------------------------------------------------
@@ -117,55 +112,50 @@ export function materializeSteps(events: Event[]): MaterializedStep[] {
117
112
  let attempt = 0;
118
113
  let startedAt: Date | undefined;
119
114
  let completedAt: Date | undefined;
120
- let updatedAt = created.createdAt;
115
+ let updatedAt = getEventTimestamp(created) ?? created.createdAt;
121
116
 
122
117
  for (const e of stepEvents) {
123
118
  switch (e.eventType) {
124
119
  case 'step_started':
125
120
  status = 'running';
126
121
  attempt += 1;
127
- if (!startedAt) startedAt = e.createdAt;
122
+ if (!startedAt) startedAt = getEventTimestamp(e) ?? e.createdAt;
128
123
  completedAt = undefined;
129
- updatedAt = e.createdAt;
124
+ updatedAt = getEventTimestamp(e) ?? e.createdAt;
130
125
  break;
131
126
  case 'step_completed':
132
127
  status = 'completed';
133
- completedAt = e.createdAt;
134
- updatedAt = e.createdAt;
128
+ completedAt = getEventTimestamp(e) ?? e.createdAt;
129
+ updatedAt = getEventTimestamp(e) ?? e.createdAt;
135
130
  break;
136
131
  case 'step_failed':
137
132
  status = 'failed';
138
- completedAt = e.createdAt;
139
- updatedAt = e.createdAt;
133
+ completedAt = getEventTimestamp(e) ?? e.createdAt;
134
+ updatedAt = getEventTimestamp(e) ?? e.createdAt;
140
135
  break;
141
136
  case 'step_retrying':
142
137
  status = 'pending';
143
138
  completedAt = undefined;
144
- updatedAt = e.createdAt;
139
+ updatedAt = getEventTimestamp(e) ?? e.createdAt;
145
140
  break;
146
141
  }
147
142
  }
148
143
 
149
- steps.push(
150
- withOccurredAt(
151
- {
152
- stepId: correlationId,
153
- runId: created.runId,
154
- stepName:
155
- created.eventType === 'step_created'
156
- ? (created.eventData?.stepName ?? correlationId)
157
- : correlationId,
158
- status,
159
- attempt,
160
- createdAt: created.createdAt,
161
- startedAt,
162
- completedAt,
163
- updatedAt,
164
- events: stepEvents,
165
- },
166
- created.occurredAt
167
- )
168
- );
144
+ steps.push({
145
+ stepId: correlationId,
146
+ runId: created.runId,
147
+ stepName:
148
+ created.eventType === 'step_created'
149
+ ? (created.eventData?.stepName ?? correlationId)
150
+ : correlationId,
151
+ status,
152
+ attempt,
153
+ createdAt: getEventTimestamp(created) ?? created.createdAt,
154
+ startedAt,
155
+ completedAt,
156
+ updatedAt,
157
+ events: stepEvents,
158
+ });
169
159
  }
170
160
 
171
161
  return steps;
@@ -192,24 +182,19 @@ export function materializeHooks(events: Event[]): MaterializedHook[] {
192
182
  const disposed = hookEvents.find((e) => e.eventType === 'hook_disposed');
193
183
  const lastReceived = receivedEvents.at(-1);
194
184
 
195
- hooks.push(
196
- withOccurredAt(
197
- {
198
- hookId: correlationId,
199
- runId: created.runId,
200
- token:
201
- created.eventType === 'hook_created'
202
- ? created.eventData?.token
203
- : undefined,
204
- createdAt: created.createdAt,
205
- receivedCount: receivedEvents.length,
206
- lastReceivedAt: lastReceived?.createdAt,
207
- disposedAt: disposed?.createdAt,
208
- events: hookEvents,
209
- },
210
- created.occurredAt
211
- )
212
- );
185
+ hooks.push({
186
+ hookId: correlationId,
187
+ runId: created.runId,
188
+ token:
189
+ created.eventType === 'hook_created'
190
+ ? created.eventData?.token
191
+ : undefined,
192
+ createdAt: getEventTimestamp(created) ?? created.createdAt,
193
+ receivedCount: receivedEvents.length,
194
+ lastReceivedAt: getEventTimestamp(lastReceived),
195
+ disposedAt: getEventTimestamp(disposed),
196
+ events: hookEvents,
197
+ });
213
198
  }
214
199
 
215
200
  return hooks;
@@ -232,23 +217,18 @@ export function materializeWaits(events: Event[]): MaterializedWait[] {
232
217
 
233
218
  const completed = waitEvents.find((e) => e.eventType === 'wait_completed');
234
219
 
235
- waits.push(
236
- withOccurredAt(
237
- {
238
- waitId: correlationId,
239
- runId: created.runId,
240
- status: completed ? 'completed' : 'waiting',
241
- createdAt: created.createdAt,
242
- resumeAt:
243
- created.eventType === 'wait_created'
244
- ? created.eventData?.resumeAt
245
- : undefined,
246
- completedAt: completed?.createdAt,
247
- events: waitEvents,
248
- },
249
- created.occurredAt
250
- )
251
- );
220
+ waits.push({
221
+ waitId: correlationId,
222
+ runId: created.runId,
223
+ status: completed ? 'completed' : 'waiting',
224
+ createdAt: getEventTimestamp(created) ?? created.createdAt,
225
+ resumeAt:
226
+ created.eventType === 'wait_created'
227
+ ? created.eventData?.resumeAt
228
+ : undefined,
229
+ completedAt: getEventTimestamp(completed),
230
+ events: waitEvents,
231
+ });
252
232
  }
253
233
 
254
234
  return waits;
@@ -9,6 +9,7 @@
9
9
  import type { Event, WorkflowRun } from '@workflow/world';
10
10
  import type { Span } from '../components/trace-viewer/types';
11
11
  import {
12
+ getEventTimestamp,
12
13
  hookToSpan,
13
14
  runToSpan,
14
15
  stepToSpan,
@@ -132,7 +133,7 @@ export function groupEventsByCorrelation(events: Event[]): GroupedEvents {
132
133
  function computeLatestKnownTime(events: Event[], run: WorkflowRun): Date {
133
134
  let latest = new Date(run.createdAt).getTime();
134
135
  for (const event of events) {
135
- const t = new Date(event.createdAt).getTime();
136
+ const t = (getEventTimestamp(event) ?? new Date(event.createdAt)).getTime();
136
137
  if (t > latest) latest = t;
137
138
  }
138
139
  return new Date(latest);
package/src/lib/utils.ts CHANGED
@@ -1,11 +1,5 @@
1
1
  import type { Step } from '@workflow/world';
2
2
  import type { ModelMessage } from 'ai';
3
- import { type ClassValue, clsx } from 'clsx';
4
- import { twMerge } from 'tailwind-merge';
5
-
6
- export function cn(...inputs: ClassValue[]) {
7
- return twMerge(clsx(inputs));
8
- }
9
3
 
10
4
  const MS_IN_SECOND = 1000;
11
5
  const MS_IN_MINUTE = 60 * MS_IN_SECOND;