@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
@@ -69,6 +69,25 @@ function formatEventTime(date: Date): string {
69
69
  );
70
70
  }
71
71
 
72
+ function parseEventDate(value: unknown): Date | null {
73
+ if (value == null) return null;
74
+
75
+ const date = value instanceof Date ? value : new Date(String(value));
76
+ return Number.isNaN(date.getTime()) ? null : date;
77
+ }
78
+
79
+ function getEffectiveEventDate(
80
+ event: Pick<Event, 'createdAt' | 'occurredAt'>
81
+ ): Date {
82
+ return parseEventDate(event.occurredAt) ?? new Date(event.createdAt);
83
+ }
84
+
85
+ function getEffectiveEventTime(
86
+ event: Pick<Event, 'createdAt' | 'occurredAt'>
87
+ ): number {
88
+ return getEffectiveEventDate(event).getTime();
89
+ }
90
+
72
91
  function formatEventType(eventType: Event['eventType']): string {
73
92
  return eventType
74
93
  .split('_')
@@ -178,7 +197,7 @@ export function buildDurationMap(events: Event[]): Map<string, DurationInfo> {
178
197
  // events for the same correlationId; the queued duration must be measured
179
198
  // against the first one, not the last.
180
199
  const chronological = [...events].sort(
181
- (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
200
+ (a, b) => getEffectiveEventTime(a) - getEffectiveEventTime(b)
182
201
  );
183
202
 
184
203
  const createdTimes = new Map<string, number>();
@@ -187,7 +206,7 @@ export function buildDurationMap(events: Event[]): Map<string, DurationInfo> {
187
206
  const durations = new Map<string, DurationInfo>();
188
207
 
189
208
  for (const event of chronological) {
190
- const ts = new Date(event.createdAt).getTime();
209
+ const ts = getEffectiveEventTime(event);
191
210
  const key = event.correlationId ?? '__run__';
192
211
  const type: string = event.eventType;
193
212
 
@@ -652,7 +671,11 @@ const SORT_OPTIONS = [
652
671
  { value: 'asc' as const, label: 'Oldest' },
653
672
  ];
654
673
 
655
- function RowsSkeleton() {
674
+ function RowsSkeleton({
675
+ showSeparateEventOccurrenceTimestamps = false,
676
+ }: {
677
+ showSeparateEventOccurrenceTimestamps?: boolean;
678
+ }) {
656
679
  return (
657
680
  <div className="flex-1 overflow-hidden">
658
681
  {Array.from({ length: 16 }, (_, i) => (
@@ -689,7 +712,12 @@ function RowsSkeleton() {
689
712
  <div className="w-5 flex-shrink-0 flex items-center justify-center">
690
713
  <Skeleton className="w-5 h-5" style={{ borderRadius: 4 }} />
691
714
  </div>
692
- {/* Time */}
715
+ {showSeparateEventOccurrenceTimestamps && (
716
+ <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
717
+ <Skeleton className="h-3" style={{ width: '70%' }} />
718
+ </div>
719
+ )}
720
+ {/* Created */}
693
721
  <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
694
722
  <Skeleton className="h-3" style={{ width: '70%' }} />
695
723
  </div>
@@ -754,9 +782,11 @@ interface EventsListProps {
754
782
  kind: ExactWorkflowSearchIdKind,
755
783
  signal?: AbortSignal
756
784
  ) => Promise<ExactIdSearchResult>;
785
+ /** Show occurredAt separately instead of folding it into the Created timestamp. */
786
+ showSeparateEventOccurrenceTimestamps?: boolean;
757
787
  }
758
788
 
759
- function EventRow({
789
+ export function EventRow({
760
790
  event,
761
791
  index,
762
792
  isFirst,
@@ -777,6 +807,7 @@ function EventRow({
777
807
  encryptionKey,
778
808
  onEncryptedDataDetected,
779
809
  suppressGroupDimming = false,
810
+ showSeparateEventOccurrenceTimestamps = false,
780
811
  }: {
781
812
  event: Event;
782
813
  index: number;
@@ -799,6 +830,8 @@ function EventRow({
799
830
  onEncryptedDataDetected?: () => void;
800
831
  /** Exact-ID search results should not dim unrelated rows. */
801
832
  suppressGroupDimming?: boolean;
833
+ /** Show occurredAt separately instead of folding it into the Created timestamp. */
834
+ showSeparateEventOccurrenceTimestamps?: boolean;
802
835
  }) {
803
836
  const [isLoading, setIsLoading] = useState(false);
804
837
  const [loadedEventData, setLoadedEventData] = useState<unknown | null>(
@@ -827,6 +860,10 @@ function EventRow({
827
860
 
828
861
  const statusDotColor = getStatusDotColor(event.eventType);
829
862
  const createdAt = new Date(event.createdAt);
863
+ const occurredAt = parseEventDate(event.occurredAt);
864
+ const displayedCreatedAt = showSeparateEventOccurrenceTimestamps
865
+ ? createdAt
866
+ : getEffectiveEventDate(event);
830
867
  const hasExistingEventData = 'eventData' in event && event.eventData != null;
831
868
  const isRun = isRunLevel(event.eventType);
832
869
  const eventName = isRun
@@ -1007,13 +1044,28 @@ function EventRow({
1007
1044
  />
1008
1045
  </div>
1009
1046
 
1010
- {/* Time */}
1047
+ {showSeparateEventOccurrenceTimestamps && (
1048
+ <div
1049
+ className="tabular-nums min-w-0 px-4"
1050
+ style={{ color: 'var(--ds-gray-900)', flex: '2 1 0%' }}
1051
+ >
1052
+ {occurredAt ? (
1053
+ <TimestampTooltip date={occurredAt}>
1054
+ <span>{formatEventTime(occurredAt)}</span>
1055
+ </TimestampTooltip>
1056
+ ) : (
1057
+ '-'
1058
+ )}
1059
+ </div>
1060
+ )}
1061
+
1062
+ {/* Created */}
1011
1063
  <div
1012
1064
  className="tabular-nums min-w-0 px-4"
1013
1065
  style={{ color: 'var(--ds-gray-900)', flex: '2 1 0%' }}
1014
1066
  >
1015
- <TimestampTooltip date={createdAt}>
1016
- <span>{formatEventTime(createdAt)}</span>
1067
+ <TimestampTooltip date={displayedCreatedAt}>
1068
+ <span>{formatEventTime(displayedCreatedAt)}</span>
1017
1069
  </TimestampTooltip>
1018
1070
  </div>
1019
1071
 
@@ -1192,6 +1244,7 @@ function EventListViewInner({
1192
1244
  isDecrypting = false,
1193
1245
  hasEncryptedData: hasEncryptedDataProp = false,
1194
1246
  onExactIdSearch,
1247
+ showSeparateEventOccurrenceTimestamps = false,
1195
1248
  }: EventsListProps) {
1196
1249
  const toast = useToast();
1197
1250
  const [internalSortOrder, setInternalSortOrder] = useState<'asc' | 'desc'>(
@@ -1229,9 +1282,7 @@ function EventListViewInner({
1229
1282
  if (sourceEvents.length === 0) return [];
1230
1283
  const dir = effectiveSortOrder === 'desc' ? -1 : 1;
1231
1284
  return [...sourceEvents].sort(
1232
- (a, b) =>
1233
- dir *
1234
- (new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
1285
+ (a, b) => dir * (getEffectiveEventTime(a) - getEffectiveEventTime(b))
1235
1286
  );
1236
1287
  }, [events, effectiveSortOrder, isExactSearchActive, searchResults]);
1237
1288
 
@@ -1645,8 +1696,13 @@ function EventListViewInner({
1645
1696
  >
1646
1697
  <div className="flex-shrink-0" style={{ width: GUTTER_WIDTH }} />
1647
1698
  <div className="w-5 flex-shrink-0" />
1699
+ {showSeparateEventOccurrenceTimestamps && (
1700
+ <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
1701
+ Occurred
1702
+ </div>
1703
+ )}
1648
1704
  <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
1649
- Time
1705
+ Created
1650
1706
  </div>
1651
1707
  <div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
1652
1708
  Event Type
@@ -1664,7 +1720,11 @@ function EventListViewInner({
1664
1720
 
1665
1721
  {/* Virtualized event rows or refetching skeleton */}
1666
1722
  {isRefetching || searchLoading ? (
1667
- <RowsSkeleton />
1723
+ <RowsSkeleton
1724
+ showSeparateEventOccurrenceTimestamps={
1725
+ showSeparateEventOccurrenceTimestamps
1726
+ }
1727
+ />
1668
1728
  ) : sortedEvents.length === 0 ? (
1669
1729
  <div
1670
1730
  className="flex flex-1 items-center justify-center px-6 text-center text-sm"
@@ -1720,6 +1780,9 @@ function EventListViewInner({
1720
1780
  encryptionKey={encryptionKey}
1721
1781
  onEncryptedDataDetected={handleEncryptedDataDetected}
1722
1782
  suppressGroupDimming={isExactSearchActive}
1783
+ showSeparateEventOccurrenceTimestamps={
1784
+ showSeparateEventOccurrenceTimestamps
1785
+ }
1723
1786
  />
1724
1787
  );
1725
1788
  }}
@@ -13,18 +13,24 @@ export {
13
13
  ResolveHookModal,
14
14
  useHookActions,
15
15
  } from './hook-actions';
16
+ export { TraceViewerSkeleton } from './new-trace-viewer/components/trace-viewer-skeleton';
16
17
  export { RunTraceView } from './run-trace-view';
17
18
  export { ConversationView } from './sidebar/conversation-view';
18
- export {
19
- SidebarDataProvider,
20
- type SidebarDataContextValue,
21
- } from './sidebar/sidebar-data-context';
22
19
  export type {
23
20
  SelectedSpanInfo,
24
21
  SpanSelectionInfo,
25
22
  } from './sidebar/entity-detail-panel';
23
+ export {
24
+ type SidebarDataContextValue,
25
+ SidebarDataProvider,
26
+ } from './sidebar/sidebar-data-context';
27
+ export type {
28
+ DetailResource,
29
+ FetchSpanDetail,
30
+ } from './sidebar/use-selected-span-detail';
26
31
  export { type StreamChunk, StreamViewer } from './stream-viewer';
27
32
  export type { Span, SpanEvent } from './trace-viewer/types';
33
+ export { NewTraceViewer } from './trace-viewer-new';
28
34
  export {
29
35
  DataInspector,
30
36
  type DataInspectorProps,
@@ -38,5 +44,3 @@ export { LoadMoreButton } from './ui/load-more-button';
38
44
  export { MenuDropdown, type MenuDropdownOption } from './ui/menu-dropdown';
39
45
  export { Spinner } from './ui/spinner';
40
46
  export { WorkflowTraceViewer } from './workflow-trace-view';
41
- export { NewTraceViewer } from './trace-viewer-new';
42
- export { TraceViewerSkeleton } from './new-trace-viewer/components/trace-viewer-skeleton';
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { Check, Copy } from 'lucide-react';
4
4
  import { type JSX, useEffect, useRef, useState } from 'react';
5
- import { cn } from '../../../lib/utils';
5
+ import { cn } from '../../../lib/cn';
6
6
 
7
7
  interface CopyButtonProps {
8
8
  copyText: string;
@@ -1,6 +1,6 @@
1
1
  import { Circle } from 'lucide-react';
2
2
  import { useRef } from 'react';
3
- import { cn } from '../../../lib/utils';
3
+ import { cn } from '../../../lib/cn';
4
4
  import type { Span } from '../types';
5
5
  import { formatDurationPrecise } from '../../trace-viewer/util/timing';
6
6
  import {
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useCallback, useRef } from 'react';
4
4
  import type { ClipboardEvent, ComponentPropsWithoutRef, JSX } from 'react';
5
- import { cn } from '../../../../lib/utils';
5
+ import { cn } from '../../../../lib/cn';
6
6
  import {
7
7
  getMiddleTruncateCopyText,
8
8
  getMiddleTruncateCopyTextFromSelectionText,
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { ArrowLeft, ArrowRight } from 'lucide-react';
4
4
  import type { ReactNode } from 'react';
5
- import { cn } from '../../../lib/utils';
5
+ import { cn } from '../../../lib/cn';
6
6
  import { TimestampTooltip } from '../../ui/timestamp-tooltip';
7
7
  import type { SpanMarker } from '../utils';
8
8
 
@@ -153,7 +153,7 @@ export function OffscreenMarkerIndicator({
153
153
  onReveal?.(targetMs);
154
154
  }}
155
155
  className={cn(
156
- 'pointer-events-auto absolute top-1/2 z-20 flex h-6 -translate-y-1/2 cursor-pointer items-center rounded-[0.25rem] border border-gray-alpha-400 bg-background-100 shadow-sm hover:bg-gray-100',
156
+ 'pointer-events-auto absolute top-1/2 z-20 flex h-6 -translate-y-1/2 cursor-pointer items-center rounded-[0.25rem] border border-gray-alpha-400 bg-background-100 hover:bg-gray-100',
157
157
  direction === 'right' ? 'right-0 pl-1.5' : 'left-0 pr-1.5'
158
158
  )}
159
159
  >
@@ -10,7 +10,7 @@ import {
10
10
  useRef,
11
11
  useState,
12
12
  } from 'react';
13
- import { cn } from '../../../lib/utils';
13
+ import { cn } from '../../../lib/cn';
14
14
 
15
15
  const GUTTER_PX = 1;
16
16
  const MIN_PX = 50;
@@ -11,7 +11,7 @@ import {
11
11
  useRef,
12
12
  useState,
13
13
  } from 'react';
14
- import { cn } from '../../../lib/utils';
14
+ import { cn } from '../../../lib/cn';
15
15
  import {
16
16
  formatDuration,
17
17
  formatDurationPrecise,
@@ -39,14 +39,14 @@ import {
39
39
  TooltipTrigger,
40
40
  } from '../ui/tooltip';
41
41
  import EventList from './components/event-list';
42
- import { TraceShortcutHelper } from './components/trace-shortcut-helper';
43
- import { ROW_HEIGHT_PX, scrollRowIntoView } from './components/use-row-window';
44
42
  import { SplitPane } from './components/split-pane';
45
43
  import {
46
44
  TIMELINE_PADDING_PX,
47
45
  Timeline,
48
46
  TimelineHeader,
49
47
  } from './components/timeline';
48
+ import { TraceShortcutHelper } from './components/trace-shortcut-helper';
49
+ import { ROW_HEIGHT_PX, scrollRowIntoView } from './components/use-row-window';
50
50
  import { ActiveSpanProvider, useActiveSpan } from './context';
51
51
  import { searchSpans } from './search';
52
52
  import type { TraceWithMeta } from './types';
@@ -822,10 +822,7 @@ function NewTraceViewerContent({
822
822
  run={sidebar.run}
823
823
  onStreamClick={sidebar.onStreamClick}
824
824
  onRunClick={sidebar.onRunClick}
825
- spanDetailData={sidebar.spanDetailData}
826
- spanDetailError={sidebar.spanDetailError}
827
- spanDetailLoading={sidebar.spanDetailLoading}
828
- onSpanSelect={sidebar.onSpanSelect}
825
+ fetchSpanDetail={sidebar.fetchSpanDetail}
829
826
  onWakeUpSleep={sidebar.onWakeUpSleep}
830
827
  onLoadEventData={sidebar.onLoadEventData}
831
828
  onResolveHook={sidebar.onResolveHook}
@@ -833,6 +830,9 @@ function NewTraceViewerContent({
833
830
  onDecrypt={sidebar.onDecrypt}
834
831
  isDecrypting={sidebar.isDecrypting}
835
832
  selectedSpan={selectedSpan}
833
+ showSeparateEventOccurrenceTimestamps={
834
+ sidebar.showSeparateEventOccurrenceTimestamps
835
+ }
836
836
  />
837
837
  </ErrorBoundary>
838
838
  </div>
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
 
3
- import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
3
+ import type { Event, Hook, WorkflowRun } from '@workflow/world';
4
4
  import { AlertCircle } from 'lucide-react';
5
- import type { SpanSelectionInfo } from './sidebar/entity-detail-panel';
5
+ import type { FetchSpanDetail } from './sidebar/use-selected-span-detail';
6
6
  import { WorkflowTraceViewer } from './workflow-trace-view';
7
7
 
8
8
  interface RunTraceViewProps {
@@ -10,9 +10,7 @@ interface RunTraceViewProps {
10
10
  events: Event[];
11
11
  isLoading?: boolean;
12
12
  error?: Error | null;
13
- spanDetailData?: WorkflowRun | Step | Hook | Event | null;
14
- spanDetailLoading?: boolean;
15
- spanDetailError?: Error | null;
13
+ fetchSpanDetail: FetchSpanDetail;
16
14
  onWakeUpSleep?: (
17
15
  runId: string,
18
16
  correlationId: string
@@ -25,10 +23,10 @@ interface RunTraceViewProps {
25
23
  onCancelRun?: (runId: string) => Promise<void>;
26
24
  onStreamClick?: (streamId: string) => void;
27
25
  onRunClick?: (runId: string) => void;
28
- onSpanSelect?: (info: SpanSelectionInfo) => void;
29
26
  onLoadMoreSpans?: () => void | Promise<void>;
30
27
  hasMoreSpans?: boolean;
31
28
  isLoadingMoreSpans?: boolean;
29
+ showSeparateEventOccurrenceTimestamps?: boolean;
32
30
  }
33
31
 
34
32
  export function RunTraceView({
@@ -36,18 +34,16 @@ export function RunTraceView({
36
34
  events,
37
35
  isLoading,
38
36
  error,
39
- spanDetailData,
40
- spanDetailLoading,
41
- spanDetailError,
37
+ fetchSpanDetail,
42
38
  onWakeUpSleep,
43
39
  onResolveHook,
44
40
  onCancelRun,
45
41
  onStreamClick,
46
42
  onRunClick,
47
- onSpanSelect,
48
43
  onLoadMoreSpans,
49
44
  hasMoreSpans,
50
45
  isLoadingMoreSpans,
46
+ showSeparateEventOccurrenceTimestamps,
51
47
  }: RunTraceViewProps) {
52
48
  if (error && !run) {
53
49
  return (
@@ -66,18 +62,18 @@ export function RunTraceView({
66
62
  events={events}
67
63
  run={run}
68
64
  isLoading={isLoading}
69
- spanDetailData={spanDetailData}
70
- spanDetailLoading={spanDetailLoading}
71
- spanDetailError={spanDetailError}
65
+ fetchSpanDetail={fetchSpanDetail}
72
66
  onWakeUpSleep={onWakeUpSleep}
73
67
  onResolveHook={onResolveHook}
74
68
  onCancelRun={onCancelRun}
75
69
  onStreamClick={onStreamClick}
76
70
  onRunClick={onRunClick}
77
- onSpanSelect={onSpanSelect}
78
71
  onLoadMoreSpans={onLoadMoreSpans}
79
72
  hasMoreSpans={hasMoreSpans}
80
73
  isLoadingMoreSpans={isLoadingMoreSpans}
74
+ showSeparateEventOccurrenceTimestamps={
75
+ showSeparateEventOccurrenceTimestamps
76
+ }
81
77
  />
82
78
  </div>
83
79
  );
@@ -768,10 +768,8 @@ export const AttributePanel = ({
768
768
 
769
769
  if (resource === 'sleep') return present;
770
770
 
771
- // During loading, ensure sections appear so their skeletons render
772
- // in the correct position (above the events section).
773
- const loadingDefaults = ['input', 'output'];
774
- for (const key of loadingDefaults) {
771
+ const loadingPlaceholders = ['input', 'output'];
772
+ for (const key of loadingPlaceholders) {
775
773
  if (!present.includes(key)) {
776
774
  present.push(key);
777
775
  }
@@ -1,6 +1,6 @@
1
1
  import { ChevronDown, ChevronRight } from 'lucide-react';
2
2
  import { type ReactNode, useState } from 'react';
3
- import { cn } from '../../lib/utils';
3
+ import { cn } from '../../lib/cn';
4
4
 
5
5
  export function DetailCard({
6
6
  summary,
@@ -1,9 +1,9 @@
1
1
  'use client';
2
2
 
3
- import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
3
+ import type { Event, Hook, WorkflowRun } from '@workflow/world';
4
4
  import clsx from 'clsx';
5
5
  import { Send, Zap } from 'lucide-react';
6
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
+ import { useCallback, useEffect, useMemo, useState } from 'react';
7
7
  import { useToast } from '../../lib/toast';
8
8
  import { DecryptClickContext } from '../ui/data-inspector';
9
9
  import { AttributePanel } from './attribute-panel';
@@ -11,19 +11,11 @@ import { EventsList } from './events-list';
11
11
  import { ResolveHookModal } from './resolve-hook-modal';
12
12
  import { useSidebarDataOptional } from './sidebar-data-context';
13
13
  import {
14
- mergeSpanDetail,
15
- spanDetailMatchesSelection,
16
- } from './span-detail-merge';
17
-
18
- // Type guards for runtime validation of span attribute data
19
- function isStep(data: unknown): data is Step {
20
- return data !== null && typeof data === 'object' && 'stepId' in data;
21
- }
22
-
23
- function isWorkflowRun(data: unknown): data is WorkflowRun {
24
- return data !== null && typeof data === 'object' && 'runId' in data;
25
- }
14
+ type FetchSpanDetail,
15
+ useSelectedSpanDetail,
16
+ } from './use-selected-span-detail';
26
17
 
18
+ // Type guard for runtime validation of span attribute data
27
19
  function isHook(data: unknown): data is Hook {
28
20
  return data !== null && typeof data === 'object' && 'hookId' in data;
29
21
  }
@@ -61,10 +53,7 @@ export function EntityDetailPanel({
61
53
  run,
62
54
  onStreamClick,
63
55
  onRunClick,
64
- spanDetailData,
65
- spanDetailError,
66
- spanDetailLoading,
67
- onSpanSelect,
56
+ fetchSpanDetail,
68
57
  onWakeUpSleep,
69
58
  onLoadEventData,
70
59
  onResolveHook,
@@ -72,20 +61,14 @@ export function EntityDetailPanel({
72
61
  onDecrypt,
73
62
  isDecrypting = false,
74
63
  selectedSpan,
64
+ showSeparateEventOccurrenceTimestamps = false,
75
65
  }: {
76
66
  run: WorkflowRun;
77
67
  /** Callback when a stream reference is clicked */
78
68
  onStreamClick?: (streamId: string) => void;
79
69
  /** Callback when a run reference is clicked */
80
70
  onRunClick?: (runId: string) => void;
81
- /** Pre-fetched span detail data for the selected span. */
82
- spanDetailData: WorkflowRun | Step | Hook | Event | null;
83
- /** Error from external span detail fetch. */
84
- spanDetailError?: Error | null;
85
- /** Loading state from external span detail fetch. */
86
- spanDetailLoading?: boolean;
87
- /** Callback when a span is selected. Use this to fetch data externally and pass via spanDetailData. */
88
- onSpanSelect: (info: SpanSelectionInfo) => void;
71
+ fetchSpanDetail: FetchSpanDetail;
89
72
  /** Callback to wake up a pending sleep call. */
90
73
  onWakeUpSleep?: (
91
74
  runId: string,
@@ -110,6 +93,8 @@ export function EntityDetailPanel({
110
93
  isDecrypting?: boolean;
111
94
  /** Info about the currently selected span from the trace viewer */
112
95
  selectedSpan: SelectedSpanInfo | null;
96
+ /** Show occurredAt separately instead of folding it into the Created timestamp. */
97
+ showSeparateEventOccurrenceTimestamps?: boolean;
113
98
  }): React.JSX.Element | null {
114
99
  const toast = useToast();
115
100
  const [stoppingSleep, setStoppingSleep] = useState(false);
@@ -128,53 +113,15 @@ export function EntityDetailPanel({
128
113
  const rawEvents = selectedSpan?.rawEvents;
129
114
  const rawEventsLength = rawEvents?.length ?? 0;
130
115
 
131
- // Determine resource type, ID, and runId from the selected span
132
- const { resource, resourceId, runId } = useMemo(() => {
133
- if (!selectedSpan) {
134
- return { resource: undefined, resourceId: undefined, runId: undefined };
135
- }
136
-
137
- const res = selectedSpan.resource;
138
- if (res === 'step' && isStep(data)) {
139
- return { resource: 'step', resourceId: data.stepId, runId: data.runId };
140
- }
141
- if (res === 'run' && isWorkflowRun(data)) {
142
- return { resource: 'run', resourceId: data.runId, runId: undefined };
143
- }
144
- if (res === 'hook' && isHook(data)) {
145
- return { resource: 'hook', resourceId: data.hookId, runId: undefined };
146
- }
147
- if (res === 'sleep') {
148
- const waitData = data as { runId?: string } | undefined;
149
- return {
150
- resource: 'sleep',
151
- resourceId: selectedSpan.spanId,
152
- runId: waitData?.runId,
153
- };
154
- }
155
- return { resource: undefined, resourceId: undefined, runId: undefined };
156
- }, [selectedSpan, data]);
157
-
158
- // Notify parent when span selection changes.
159
- // Use a ref for the callback so the effect only fires when the actual
160
- // selection values change, not when the callback identity changes due to
161
- // parent re-renders from polling.
162
- const onSpanSelectRef = useRef(onSpanSelect);
163
- onSpanSelectRef.current = onSpanSelect;
164
-
165
- useEffect(() => {
166
- if (
167
- resource &&
168
- resourceId &&
169
- ['run', 'step', 'hook', 'sleep'].includes(resource)
170
- ) {
171
- onSpanSelectRef.current({
172
- resource: resource as 'run' | 'step' | 'hook' | 'sleep',
173
- resourceId,
174
- runId,
175
- });
176
- }
177
- }, [resource, resourceId, runId]);
116
+ const {
117
+ status,
118
+ resource,
119
+ resourceId,
120
+ displayData,
121
+ detail: matchedSpanDetailData,
122
+ error,
123
+ } = useSelectedSpanDetail(selectedSpan, fetchSpanDetail);
124
+ const loading = status === 'loading';
178
125
 
179
126
  // Check if this sleep is still pending and can be woken up
180
127
  const canWakeUp = useMemo(() => {
@@ -216,17 +163,6 @@ export function EntityDetailPanel({
216
163
  resolvedHookIds,
217
164
  ]);
218
165
 
219
- const error = spanDetailError ?? undefined;
220
- const loading = spanDetailLoading ?? false;
221
-
222
- const matchedSpanDetailData = useMemo(
223
- () =>
224
- spanDetailMatchesSelection(spanDetailData, resource, resourceId)
225
- ? spanDetailData
226
- : null,
227
- [spanDetailData, resource, resourceId]
228
- );
229
-
230
166
  // Get the hook token for resolving (prefer fetched data, then hooks array fallback)
231
167
  const hookToken = useMemo(() => {
232
168
  if (resource !== 'hook' || !resourceId) return undefined;
@@ -325,16 +261,6 @@ export function EntityDetailPanel({
325
261
  [onResolveHook, hookToken, resolvingHook, matchedSpanDetailData, data]
326
262
  );
327
263
 
328
- const displayData = useMemo(
329
- () =>
330
- mergeSpanDetail(data, matchedSpanDetailData) as
331
- | WorkflowRun
332
- | Step
333
- | Hook
334
- | Event,
335
- [data, matchedSpanDetailData]
336
- );
337
-
338
264
  const moduleSpecifier = useMemo(() => {
339
265
  const displayRecord = displayData as Record<string, unknown>;
340
266
  const displayStepName = displayRecord.stepName;
@@ -451,6 +377,9 @@ export function EntityDetailPanel({
451
377
  onStreamClick={onStreamClick}
452
378
  onRunClick={onRunClick}
453
379
  encryptionKey={encryptionKey}
380
+ showSeparateEventOccurrenceTimestamps={
381
+ showSeparateEventOccurrenceTimestamps
382
+ }
454
383
  />
455
384
  )}
456
385
  </div>