@workflow/web-shared 5.0.0-beta.23 → 5.0.0-beta.25

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 (94) 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 -1
  49. package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
  50. package/dist/components/workflow-traces/trace-span-construction.js +69 -29
  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.map +1 -1
  55. package/dist/lib/event-materialization.js +22 -15
  56. package/dist/lib/trace-builder.d.ts.map +1 -1
  57. package/dist/lib/trace-builder.js +3 -3
  58. package/dist/lib/utils.d.ts +0 -2
  59. package/dist/lib/utils.d.ts.map +1 -1
  60. package/dist/lib/utils.js +1 -6
  61. package/package.json +3 -3
  62. package/src/components/event-list-view.tsx +76 -13
  63. package/src/components/index.ts +10 -6
  64. package/src/components/new-trace-viewer/components/copy-button.tsx +1 -1
  65. package/src/components/new-trace-viewer/components/event-list.tsx +1 -1
  66. package/src/components/new-trace-viewer/components/middle-truncate/middle-truncate.tsx +1 -1
  67. package/src/components/new-trace-viewer/components/span-markers.tsx +2 -2
  68. package/src/components/new-trace-viewer/components/split-pane.tsx +1 -1
  69. package/src/components/new-trace-viewer/components/timeline.tsx +1 -1
  70. package/src/components/new-trace-viewer/trace-viewer.tsx +6 -6
  71. package/src/components/run-trace-view.tsx +10 -14
  72. package/src/components/sidebar/attribute-panel.tsx +2 -4
  73. package/src/components/sidebar/detail-card.tsx +1 -1
  74. package/src/components/sidebar/entity-detail-panel.tsx +23 -94
  75. package/src/components/sidebar/events-list.tsx +58 -8
  76. package/src/components/sidebar/sidebar-data-context.tsx +6 -7
  77. package/src/components/sidebar/span-detail-merge.ts +69 -0
  78. package/src/components/sidebar/use-selected-span-detail.ts +146 -0
  79. package/src/components/ui/alert.tsx +1 -1
  80. package/src/components/ui/button.tsx +1 -1
  81. package/src/components/ui/card.tsx +1 -1
  82. package/src/components/ui/context-card.tsx +1 -1
  83. package/src/components/ui/error-card.tsx +1 -1
  84. package/src/components/ui/icon-button.tsx +1 -1
  85. package/src/components/ui/kbd.tsx +1 -1
  86. package/src/components/ui/skeleton.tsx +1 -1
  87. package/src/components/ui/tooltip.tsx +1 -1
  88. package/src/components/workflow-trace-view.tsx +11 -21
  89. package/src/components/workflow-traces/trace-span-construction.test.ts +85 -0
  90. package/src/components/workflow-traces/trace-span-construction.ts +93 -29
  91. package/src/lib/cn.ts +93 -0
  92. package/src/lib/event-materialization.ts +22 -14
  93. package/src/lib/trace-builder.ts +2 -1
  94. package/src/lib/utils.ts +0 -6
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name';
4
- import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
4
+ import type { Event, Hook, WorkflowRun } from '@workflow/world';
5
5
  import {
6
6
  ChevronDown,
7
7
  ChevronUp,
@@ -29,6 +29,7 @@ import {
29
29
  type SpanSelectionInfo,
30
30
  } from './sidebar/entity-detail-panel';
31
31
  import { ResolveHookModal } from './sidebar/resolve-hook-modal';
32
+ import type { FetchSpanDetail } from './sidebar/use-selected-span-detail';
32
33
  import {
33
34
  TraceViewerContextProvider,
34
35
  TraceViewerTimeline,
@@ -764,15 +765,12 @@ export const WorkflowTraceViewer = ({
764
765
  events,
765
766
  isLoading,
766
767
  error,
767
- spanDetailData,
768
- spanDetailLoading,
769
- spanDetailError,
768
+ fetchSpanDetail,
770
769
  onWakeUpSleep,
771
770
  onResolveHook,
772
771
  onCancelRun,
773
772
  onStreamClick,
774
773
  onRunClick,
775
- onSpanSelect,
776
774
  onLoadEventData,
777
775
  onLoadMoreSpans,
778
776
  hasMoreSpans = false,
@@ -780,14 +778,13 @@ export const WorkflowTraceViewer = ({
780
778
  encryptionKey,
781
779
  onDecrypt,
782
780
  isDecrypting = false,
781
+ showSeparateEventOccurrenceTimestamps = false,
783
782
  }: {
784
783
  run: WorkflowRun;
785
784
  events: Event[];
786
785
  isLoading?: boolean;
787
786
  error?: Error | null;
788
- spanDetailData?: WorkflowRun | Step | Hook | Event | null;
789
- spanDetailLoading?: boolean;
790
- spanDetailError?: Error | null;
787
+ fetchSpanDetail: FetchSpanDetail;
791
788
  onWakeUpSleep?: (
792
789
  runId: string,
793
790
  correlationId: string
@@ -803,8 +800,6 @@ export const WorkflowTraceViewer = ({
803
800
  onStreamClick?: (streamId: string) => void;
804
801
  /** Callback when a run reference is clicked in the detail panel */
805
802
  onRunClick?: (runId: string) => void;
806
- /** Callback when a span is selected. */
807
- onSpanSelect?: (info: SpanSelectionInfo) => void;
808
803
  /** Callback to load event data for a specific event (lazy loading in sidebar) */
809
804
  onLoadEventData?: (
810
805
  correlationId: string,
@@ -822,6 +817,8 @@ export const WorkflowTraceViewer = ({
822
817
  onDecrypt?: () => void;
823
818
  /** Whether the encryption key is currently being fetched */
824
819
  isDecrypting?: boolean;
820
+ /** Show occurredAt separately instead of folding it into the Created timestamp. */
821
+ showSeparateEventOccurrenceTimestamps?: boolean;
825
822
  }) => {
826
823
  const toast = useToast();
827
824
  const [selectedSpan, setSelectedSpan] = useState<SelectedSpanInfo | null>(
@@ -856,13 +853,6 @@ export const WorkflowTraceViewer = ({
856
853
  }
857
854
  }, [error, isLoading]);
858
855
 
859
- const handleSpanSelect = useCallback(
860
- (info: SpanSelectionInfo) => {
861
- onSpanSelect?.(info);
862
- },
863
- [onSpanSelect]
864
- );
865
-
866
856
  const handleSelectionChange = useCallback(
867
857
  (info: SelectedSpanInfo | null) => {
868
858
  if (info) {
@@ -1158,10 +1148,7 @@ export const WorkflowTraceViewer = ({
1158
1148
  run={run}
1159
1149
  onStreamClick={onStreamClick}
1160
1150
  onRunClick={onRunClick}
1161
- spanDetailData={spanDetailData ?? null}
1162
- spanDetailError={spanDetailError}
1163
- spanDetailLoading={spanDetailLoading}
1164
- onSpanSelect={handleSpanSelect}
1151
+ fetchSpanDetail={fetchSpanDetail}
1165
1152
  onWakeUpSleep={onWakeUpSleep}
1166
1153
  onLoadEventData={onLoadEventData}
1167
1154
  onResolveHook={onResolveHook}
@@ -1169,6 +1156,9 @@ export const WorkflowTraceViewer = ({
1169
1156
  onDecrypt={onDecrypt}
1170
1157
  isDecrypting={isDecrypting}
1171
1158
  selectedSpan={selectedSpan}
1159
+ showSeparateEventOccurrenceTimestamps={
1160
+ showSeparateEventOccurrenceTimestamps
1161
+ }
1172
1162
  />
1173
1163
  </ErrorBoundary>
1174
1164
  </div>
@@ -0,0 +1,85 @@
1
+ import type { Event, WorkflowRun } from '@workflow/world';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { runToSpan } from './trace-span-construction';
4
+ import { otelTimeToMs } from './trace-time-utils';
5
+
6
+ const date = (ms: number) => new Date(ms);
7
+
8
+ function run(overrides: Partial<WorkflowRun> = {}): WorkflowRun {
9
+ return {
10
+ runId: 'run_1',
11
+ status: 'completed',
12
+ deploymentId: 'dpl_1',
13
+ workflowName: 'workflow//./workflow//testWorkflow',
14
+ specVersion: 2,
15
+ attributes: {},
16
+ createdAt: date(1_000),
17
+ updatedAt: date(6_000),
18
+ startedAt: date(2_000),
19
+ completedAt: date(6_000),
20
+ output: null,
21
+ ...overrides,
22
+ } as WorkflowRun;
23
+ }
24
+
25
+ function event(overrides: Partial<Event>): Event {
26
+ return {
27
+ runId: 'run_1',
28
+ eventId: `evt_${overrides.eventType ?? 'unknown'}`,
29
+ eventType: 'run_created',
30
+ createdAt: date(1_000),
31
+ specVersion: 2,
32
+ eventData: {
33
+ deploymentId: 'dpl_1',
34
+ workflowName: 'workflow//./workflow//testWorkflow',
35
+ input: null,
36
+ },
37
+ ...overrides,
38
+ } as Event;
39
+ }
40
+
41
+ describe('runToSpan', () => {
42
+ it('prefers occurredAt for run span timing when available', () => {
43
+ const span = runToSpan(
44
+ run(),
45
+ [
46
+ event({
47
+ eventType: 'run_created',
48
+ createdAt: date(1_000),
49
+ occurredAt: date(100),
50
+ }),
51
+ event({
52
+ eventType: 'run_started',
53
+ createdAt: date(2_000),
54
+ occurredAt: date(200),
55
+ }),
56
+ event({
57
+ eventType: 'run_completed',
58
+ createdAt: date(6_000),
59
+ occurredAt: date(500),
60
+ }),
61
+ ],
62
+ date(10_000)
63
+ );
64
+
65
+ expect(otelTimeToMs(span.startTime)).toBe(100);
66
+ expect(
67
+ span.activeStartTime ? otelTimeToMs(span.activeStartTime) : undefined
68
+ ).toBe(200);
69
+ expect(otelTimeToMs(span.endTime)).toBe(500);
70
+ expect(
71
+ span.events.find((e) => e.name === 'run_created')?.timestamp
72
+ ).toEqual([0, 100_000_000]);
73
+
74
+ const data = span.attributes.data as {
75
+ createdAt: Date;
76
+ startedAt?: Date;
77
+ completedAt?: Date;
78
+ occurredAt?: Date;
79
+ };
80
+ expect(data.createdAt).toEqual(date(100));
81
+ expect(data.startedAt).toEqual(date(200));
82
+ expect(data.completedAt).toEqual(date(500));
83
+ expect(data).not.toHaveProperty('occurredAt');
84
+ });
85
+ });
@@ -29,13 +29,24 @@ const MARKER_EVENT_TYPES: Set<Event['eventType']> = new Set([
29
29
  'attr_set',
30
30
  ]);
31
31
 
32
+ export const getEventTimestamp = (
33
+ event: Event | undefined
34
+ ): Date | undefined => {
35
+ const value = event?.occurredAt ?? event?.createdAt;
36
+ if (!value) return undefined;
37
+
38
+ const date = value instanceof Date ? value : new Date(value);
39
+ return Number.isNaN(date.getTime()) ? undefined : date;
40
+ };
41
+
32
42
  /**
33
43
  * Convert workflow events to span events
34
44
  * Only includes events that should be displayed as markers
35
45
  */
36
46
  export function convertEventsToSpanEvents(
37
47
  events: Event[],
38
- filterTypes = true
48
+ filterTypes = true,
49
+ options: { preferOccurredAt?: boolean } = {}
39
50
  ): SpanEvent[] {
40
51
  return events
41
52
  .filter((event) =>
@@ -43,7 +54,11 @@ export function convertEventsToSpanEvents(
43
54
  )
44
55
  .map((event) => ({
45
56
  name: event.eventType,
46
- timestamp: dateToOtelTime(event.createdAt),
57
+ timestamp: dateToOtelTime(
58
+ options.preferOccurredAt
59
+ ? (getEventTimestamp(event) ?? event.createdAt)
60
+ : event.createdAt
61
+ ),
47
62
  attributes: {
48
63
  eventId: event.eventId,
49
64
  correlationId: event.correlationId,
@@ -73,11 +88,11 @@ export const waitEventsToWaitEntity = (
73
88
  return {
74
89
  waitId: startEvent.correlationId,
75
90
  runId: startEvent.runId,
76
- createdAt: startEvent.createdAt,
91
+ createdAt: getEventTimestamp(startEvent) ?? startEvent.createdAt,
77
92
  resumeAt: startEvent.eventData?.resumeAt
78
93
  ? new Date(startEvent.eventData.resumeAt)
79
94
  : undefined,
80
- completedAt: completedEvent?.createdAt,
95
+ completedAt: getEventTimestamp(completedEvent),
81
96
  };
82
97
  };
83
98
 
@@ -169,16 +184,16 @@ export const stepEventsToStepEntity = (
169
184
  case 'step_started':
170
185
  status = 'running';
171
186
  attempt += 1;
172
- if (!startedAt) startedAt = e.createdAt;
187
+ if (!startedAt) startedAt = getEventTimestamp(e) ?? e.createdAt;
173
188
  completedAt = undefined;
174
189
  break;
175
190
  case 'step_completed':
176
191
  status = 'completed';
177
- completedAt = e.createdAt;
192
+ completedAt = getEventTimestamp(e) ?? e.createdAt;
178
193
  break;
179
194
  case 'step_failed':
180
195
  status = 'failed';
181
- completedAt = e.createdAt;
196
+ completedAt = getEventTimestamp(e) ?? e.createdAt;
182
197
  break;
183
198
  case 'step_retrying':
184
199
  status = 'pending';
@@ -191,14 +206,18 @@ export const stepEventsToStepEntity = (
191
206
  if (attempt === 0) attempt = 1;
192
207
 
193
208
  const lastEvent = events[events.length - 1];
209
+
194
210
  return {
195
211
  stepId: anchorEvent.correlationId ?? '',
196
212
  runId: anchorEvent.runId,
197
213
  stepName: createdEvent?.eventData?.stepName ?? '',
198
214
  status,
199
215
  attempt,
200
- createdAt: anchorEvent.createdAt,
201
- updatedAt: lastEvent?.createdAt ?? anchorEvent.createdAt,
216
+ createdAt: getEventTimestamp(anchorEvent) ?? anchorEvent.createdAt,
217
+ updatedAt:
218
+ getEventTimestamp(lastEvent) ??
219
+ lastEvent?.createdAt ??
220
+ anchorEvent.createdAt,
202
221
  startedAt,
203
222
  completedAt,
204
223
  specVersion: anchorEvent.specVersion,
@@ -221,25 +240,41 @@ export function stepToSpan(stepEvents: Event[], maxEndTime: Date): Span | null {
221
240
  };
222
241
 
223
242
  const resource = 'step';
224
- const endTime = new Date(step.completedAt ?? maxEndTime);
225
243
 
226
244
  // Include ALL correlated events on the span so the sidebar detail view
227
245
  // can display them. The timeline uses the `showVerticalLine` flag to
228
246
  // determine which events appear as markers.
229
- const events = convertEventsToSpanEvents(stepEvents, false);
247
+ const events = convertEventsToSpanEvents(stepEvents, false, {
248
+ preferOccurredAt: true,
249
+ });
230
250
 
231
- // Use createdAt as span start time, with activeStartTime for when execution began
232
- // This allows visualization of the "queued" period before execution
233
- 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);
234
258
  let activeStartTime = step.startedAt ? new Date(step.startedAt) : undefined;
235
259
  const firstStartEvent = stepEvents.find(
236
260
  (event) => event.eventType === 'step_started'
237
261
  );
238
262
  if (firstStartEvent) {
239
- // `step.startedAt` is the server-side creation timestamp, and `event.createdAt` is
240
- // the client-side creation timestamp. For now, to align the event marker with the
241
- // line we show for step.startedAt, we overwrite here to always use client-side time.
242
- 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);
243
278
  }
244
279
 
245
280
  return {
@@ -292,10 +327,10 @@ export const hookEventsToHookEntity = (
292
327
  hookId: createdEvent.correlationId,
293
328
  runId: createdEvent.runId,
294
329
  token: createdEvent.eventData?.token,
295
- createdAt: createdEvent.createdAt,
330
+ createdAt: getEventTimestamp(createdEvent) ?? createdEvent.createdAt,
296
331
  receivedCount: receivedEvents.length,
297
- lastReceivedAt: lastReceivedEvent?.createdAt || undefined,
298
- disposedAt: disposedEvents.at(-1)?.createdAt || undefined,
332
+ lastReceivedAt: getEventTimestamp(lastReceivedEvent),
333
+ disposedAt: getEventTimestamp(disposedEvents.at(-1)),
299
334
  };
300
335
  };
301
336
 
@@ -343,21 +378,50 @@ export function runToSpan(
343
378
  ): Span {
344
379
  const now = nowTime ?? new Date();
345
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
+
346
407
  // Only embed identification fields — not the full object with
347
- // 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.
348
410
  const { input: _i, output: _o, error: _e, ...runIdentity } = run;
349
411
  const attributes = {
350
412
  resource: 'run' as const,
351
- data: runIdentity,
413
+ data: {
414
+ ...runIdentity,
415
+ createdAt: spanStartTime,
416
+ startedAt: activeStartTime,
417
+ completedAt,
418
+ },
352
419
  };
353
420
 
354
- // Use createdAt as span start time, with activeStartTime for when execution began
355
- const spanStartTime = new Date(run.createdAt);
356
- const activeStartTime = run.startedAt ? new Date(run.startedAt) : undefined;
357
- const endTime = run.completedAt ?? now;
358
-
359
421
  // Convert run-level events to span events
360
- const events = convertEventsToSpanEvents(runEvents, false);
422
+ const events = convertEventsToSpanEvents(runEvents, false, {
423
+ preferOccurredAt: true,
424
+ });
361
425
 
362
426
  return {
363
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 };
@@ -82,6 +82,14 @@ function groupByCorrelationId(
82
82
  return groups;
83
83
  }
84
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
+
85
93
  // ---------------------------------------------------------------------------
86
94
  // materializeSteps
87
95
  // ---------------------------------------------------------------------------
@@ -104,31 +112,31 @@ export function materializeSteps(events: Event[]): MaterializedStep[] {
104
112
  let attempt = 0;
105
113
  let startedAt: Date | undefined;
106
114
  let completedAt: Date | undefined;
107
- let updatedAt = created.createdAt;
115
+ let updatedAt = getEventTimestamp(created) ?? created.createdAt;
108
116
 
109
117
  for (const e of stepEvents) {
110
118
  switch (e.eventType) {
111
119
  case 'step_started':
112
120
  status = 'running';
113
121
  attempt += 1;
114
- if (!startedAt) startedAt = e.createdAt;
122
+ if (!startedAt) startedAt = getEventTimestamp(e) ?? e.createdAt;
115
123
  completedAt = undefined;
116
- updatedAt = e.createdAt;
124
+ updatedAt = getEventTimestamp(e) ?? e.createdAt;
117
125
  break;
118
126
  case 'step_completed':
119
127
  status = 'completed';
120
- completedAt = e.createdAt;
121
- updatedAt = e.createdAt;
128
+ completedAt = getEventTimestamp(e) ?? e.createdAt;
129
+ updatedAt = getEventTimestamp(e) ?? e.createdAt;
122
130
  break;
123
131
  case 'step_failed':
124
132
  status = 'failed';
125
- completedAt = e.createdAt;
126
- updatedAt = e.createdAt;
133
+ completedAt = getEventTimestamp(e) ?? e.createdAt;
134
+ updatedAt = getEventTimestamp(e) ?? e.createdAt;
127
135
  break;
128
136
  case 'step_retrying':
129
137
  status = 'pending';
130
138
  completedAt = undefined;
131
- updatedAt = e.createdAt;
139
+ updatedAt = getEventTimestamp(e) ?? e.createdAt;
132
140
  break;
133
141
  }
134
142
  }
@@ -142,7 +150,7 @@ export function materializeSteps(events: Event[]): MaterializedStep[] {
142
150
  : correlationId,
143
151
  status,
144
152
  attempt,
145
- createdAt: created.createdAt,
153
+ createdAt: getEventTimestamp(created) ?? created.createdAt,
146
154
  startedAt,
147
155
  completedAt,
148
156
  updatedAt,
@@ -181,10 +189,10 @@ export function materializeHooks(events: Event[]): MaterializedHook[] {
181
189
  created.eventType === 'hook_created'
182
190
  ? created.eventData?.token
183
191
  : undefined,
184
- createdAt: created.createdAt,
192
+ createdAt: getEventTimestamp(created) ?? created.createdAt,
185
193
  receivedCount: receivedEvents.length,
186
- lastReceivedAt: lastReceived?.createdAt,
187
- disposedAt: disposed?.createdAt,
194
+ lastReceivedAt: getEventTimestamp(lastReceived),
195
+ disposedAt: getEventTimestamp(disposed),
188
196
  events: hookEvents,
189
197
  });
190
198
  }
@@ -213,12 +221,12 @@ export function materializeWaits(events: Event[]): MaterializedWait[] {
213
221
  waitId: correlationId,
214
222
  runId: created.runId,
215
223
  status: completed ? 'completed' : 'waiting',
216
- createdAt: created.createdAt,
224
+ createdAt: getEventTimestamp(created) ?? created.createdAt,
217
225
  resumeAt:
218
226
  created.eventType === 'wait_created'
219
227
  ? created.eventData?.resumeAt
220
228
  : undefined,
221
- completedAt: completed?.createdAt,
229
+ completedAt: getEventTimestamp(completed),
222
230
  events: waitEvents,
223
231
  });
224
232
  }
@@ -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;