@workflow/web-shared 4.1.0-beta.50 → 4.1.0-beta.52

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 (62) hide show
  1. package/dist/components/event-list-view.d.ts.map +1 -1
  2. package/dist/components/event-list-view.js +8 -5
  3. package/dist/components/event-list-view.js.map +1 -1
  4. package/dist/components/index.d.ts +3 -2
  5. package/dist/components/index.d.ts.map +1 -1
  6. package/dist/components/index.js +1 -1
  7. package/dist/components/index.js.map +1 -1
  8. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  9. package/dist/components/sidebar/attribute-panel.js +23 -91
  10. package/dist/components/sidebar/attribute-panel.js.map +1 -1
  11. package/dist/components/sidebar/detail-card.d.ts +3 -1
  12. package/dist/components/sidebar/detail-card.d.ts.map +1 -1
  13. package/dist/components/sidebar/detail-card.js +2 -2
  14. package/dist/components/sidebar/detail-card.js.map +1 -1
  15. package/dist/components/sidebar/entity-detail-panel.d.ts +22 -2
  16. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  17. package/dist/components/sidebar/entity-detail-panel.js +38 -49
  18. package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
  19. package/dist/components/sidebar/events-list.d.ts +4 -4
  20. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  21. package/dist/components/sidebar/events-list.js +77 -12
  22. package/dist/components/sidebar/events-list.js.map +1 -1
  23. package/dist/components/stream-viewer.d.ts +8 -7
  24. package/dist/components/stream-viewer.d.ts.map +1 -1
  25. package/dist/components/stream-viewer.js +12 -9
  26. package/dist/components/stream-viewer.js.map +1 -1
  27. package/dist/components/trace-viewer/context.d.ts +3 -2
  28. package/dist/components/trace-viewer/context.d.ts.map +1 -1
  29. package/dist/components/trace-viewer/context.js.map +1 -1
  30. package/dist/components/ui/inspector-theme.d.ts +81 -0
  31. package/dist/components/ui/inspector-theme.d.ts.map +1 -0
  32. package/dist/components/ui/inspector-theme.js +63 -0
  33. package/dist/components/ui/inspector-theme.js.map +1 -0
  34. package/dist/components/workflow-trace-view.d.ts +3 -1
  35. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  36. package/dist/components/workflow-trace-view.js +111 -10
  37. package/dist/components/workflow-trace-view.js.map +1 -1
  38. package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
  39. package/dist/components/workflow-traces/trace-span-construction.js +17 -12
  40. package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +1 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/lib/hydration.d.ts +26 -0
  46. package/dist/lib/hydration.d.ts.map +1 -0
  47. package/dist/lib/hydration.js +98 -0
  48. package/dist/lib/hydration.js.map +1 -0
  49. package/package.json +3 -2
  50. package/src/components/event-list-view.tsx +15 -9
  51. package/src/components/index.ts +6 -5
  52. package/src/components/sidebar/attribute-panel.tsx +30 -129
  53. package/src/components/sidebar/detail-card.tsx +7 -1
  54. package/src/components/sidebar/entity-detail-panel.tsx +71 -57
  55. package/src/components/sidebar/events-list.tsx +204 -88
  56. package/src/components/stream-viewer.tsx +37 -21
  57. package/src/components/trace-viewer/context.tsx +3 -2
  58. package/src/components/ui/inspector-theme.ts +64 -0
  59. package/src/components/workflow-trace-view.tsx +210 -32
  60. package/src/components/workflow-traces/trace-span-construction.ts +17 -12
  61. package/src/index.ts +13 -0
  62. package/src/lib/hydration.ts +137 -0
@@ -1,25 +1,210 @@
1
1
  'use client';
2
2
 
3
- import { useMemo } from 'react';
4
- import { ErrorCard } from '../ui/error-card';
5
- import type { SpanEvent } from '../trace-viewer/types.js';
6
- import { AttributeBlock, localMillisecondTime } from './attribute-panel';
3
+ import type { Event } from '@workflow/world';
4
+ import { useCallback, useMemo, useState } from 'react';
5
+ import { ObjectInspector } from 'react-inspector';
6
+ import { useDarkMode } from '../../hooks/use-dark-mode';
7
+ import { inspectorThemeDark, inspectorThemeLight } from '../ui/inspector-theme';
8
+ import { localMillisecondTime } from './attribute-panel';
7
9
  import { DetailCard } from './detail-card';
8
10
 
11
+ /**
12
+ * Event types that carry user-serialized data in their eventData field.
13
+ */
14
+ const DATA_EVENT_TYPES = new Set([
15
+ 'step_created',
16
+ 'step_completed',
17
+ 'step_failed',
18
+ 'run_created',
19
+ 'run_completed',
20
+ ]);
21
+
22
+ /**
23
+ * A single event row that can lazy-load its eventData when expanded.
24
+ */
25
+ function EventItem({
26
+ event,
27
+ onLoadEventData,
28
+ }: {
29
+ event: Event;
30
+ onLoadEventData?: (
31
+ correlationId: string,
32
+ eventId: string
33
+ ) => Promise<unknown | null>;
34
+ }) {
35
+ const isDark = useDarkMode();
36
+ const [loadedData, setLoadedData] = useState<unknown | null>(null);
37
+ const [isLoading, setIsLoading] = useState(false);
38
+ const [loadError, setLoadError] = useState<string | null>(null);
39
+
40
+ // Check if the event already has eventData from the store
41
+ const existingData =
42
+ 'eventData' in event && event.eventData != null ? event.eventData : null;
43
+ const displayData = existingData ?? loadedData;
44
+ const canHaveData = DATA_EVENT_TYPES.has(event.eventType);
45
+
46
+ const handleExpand = useCallback(async () => {
47
+ if (existingData || loadedData !== null || isLoading) return;
48
+ if (!onLoadEventData || !event.correlationId || !event.eventId) return;
49
+
50
+ try {
51
+ setIsLoading(true);
52
+ setLoadError(null);
53
+ const data = await onLoadEventData(event.correlationId, event.eventId);
54
+ setLoadedData(data);
55
+ } catch (err) {
56
+ setLoadError(err instanceof Error ? err.message : String(err));
57
+ } finally {
58
+ setIsLoading(false);
59
+ }
60
+ }, [
61
+ existingData,
62
+ loadedData,
63
+ isLoading,
64
+ onLoadEventData,
65
+ event.correlationId,
66
+ event.eventId,
67
+ ]);
68
+
69
+ const createdAt = new Date(event.createdAt);
70
+
71
+ return (
72
+ <DetailCard
73
+ summary={
74
+ <>
75
+ <span
76
+ className="font-medium"
77
+ style={{ color: 'var(--ds-gray-1000)' }}
78
+ >
79
+ {event.eventType}
80
+ </span>{' '}
81
+ -{' '}
82
+ <span style={{ color: 'var(--ds-gray-700)' }}>
83
+ {localMillisecondTime(createdAt.getTime())}
84
+ </span>
85
+ </>
86
+ }
87
+ onToggle={
88
+ canHaveData
89
+ ? (open) => {
90
+ if (open) handleExpand();
91
+ }
92
+ : undefined
93
+ }
94
+ >
95
+ {/* Event attributes */}
96
+ <div
97
+ className="flex flex-col divide-y rounded-md border overflow-hidden"
98
+ style={{
99
+ borderColor: 'var(--ds-gray-300)',
100
+ backgroundColor: 'var(--ds-gray-100)',
101
+ }}
102
+ >
103
+ <div
104
+ className="flex items-center justify-between px-2.5 py-1.5"
105
+ style={{ borderColor: 'var(--ds-gray-300)' }}
106
+ >
107
+ <span
108
+ className="text-[11px] font-medium"
109
+ style={{ color: 'var(--ds-gray-700)' }}
110
+ >
111
+ eventId
112
+ </span>
113
+ <span
114
+ className="text-[11px] font-mono"
115
+ style={{ color: 'var(--ds-gray-1000)' }}
116
+ >
117
+ {event.eventId}
118
+ </span>
119
+ </div>
120
+ {event.correlationId && (
121
+ <div
122
+ className="flex items-center justify-between px-2.5 py-1.5"
123
+ style={{ borderColor: 'var(--ds-gray-300)' }}
124
+ >
125
+ <span
126
+ className="text-[11px] font-medium"
127
+ style={{ color: 'var(--ds-gray-700)' }}
128
+ >
129
+ correlationId
130
+ </span>
131
+ <span
132
+ className="text-[11px] font-mono"
133
+ style={{ color: 'var(--ds-gray-1000)' }}
134
+ >
135
+ {event.correlationId}
136
+ </span>
137
+ </div>
138
+ )}
139
+ </div>
140
+
141
+ {/* Loading state */}
142
+ {isLoading && (
143
+ <div
144
+ className="mt-2 text-xs rounded-md border p-2"
145
+ style={{
146
+ borderColor: 'var(--ds-gray-300)',
147
+ color: 'var(--ds-gray-600)',
148
+ }}
149
+ >
150
+ Loading event data...
151
+ </div>
152
+ )}
153
+
154
+ {/* Error state */}
155
+ {loadError && (
156
+ <div
157
+ className="mt-2 text-xs rounded-md border p-2"
158
+ style={{
159
+ borderColor: 'var(--ds-red-300)',
160
+ color: 'var(--ds-red-700)',
161
+ }}
162
+ >
163
+ {loadError}
164
+ </div>
165
+ )}
166
+
167
+ {/* Event data */}
168
+ {displayData != null && (
169
+ <div
170
+ className="mt-2 overflow-x-auto rounded-md border p-3"
171
+ style={{ borderColor: 'var(--ds-gray-300)' }}
172
+ >
173
+ <ObjectInspector
174
+ data={displayData}
175
+ // @ts-expect-error react-inspector accepts theme objects at runtime
176
+ // see https://github.com/storybookjs/react-inspector/blob/main/README.md#theme
177
+ theme={isDark ? inspectorThemeDark : inspectorThemeLight}
178
+ expandLevel={2}
179
+ />
180
+ </div>
181
+ )}
182
+ </DetailCard>
183
+ );
184
+ }
185
+
9
186
  export function EventsList({
10
187
  events,
11
- fullEvents,
12
188
  isLoading = false,
13
189
  error,
190
+ onLoadEventData,
14
191
  }: {
15
- events: SpanEvent[];
16
- fullEvents?: SpanEvent[] | null;
192
+ events: Event[];
17
193
  isLoading?: boolean;
18
194
  error?: Error | null;
195
+ onLoadEventData?: (
196
+ correlationId: string,
197
+ eventId: string
198
+ ) => Promise<unknown | null>;
19
199
  }) {
20
- const displayData = useMemo(
21
- () => (fullEvents?.length ? fullEvents : events) || [],
22
- [events, fullEvents]
200
+ // Sort by createdAt
201
+ const sortedEvents = useMemo(
202
+ () =>
203
+ [...events].sort(
204
+ (a, b) =>
205
+ new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
206
+ ),
207
+ [events]
23
208
  );
24
209
 
25
210
  return (
@@ -28,89 +213,20 @@ export function EventsList({
28
213
  className="text-heading-16 font-medium mt-4 mb-2"
29
214
  style={{ color: 'var(--ds-gray-1000)' }}
30
215
  >
31
- Events {!isLoading && `(${displayData.length})`}
216
+ Events {!isLoading && `(${sortedEvents.length})`}
32
217
  </h3>
33
- {error ? (
34
- <ErrorCard
35
- title="Failed to load full event list"
36
- details={error?.message}
37
- className="my-4"
38
- />
39
- ) : null}
40
218
  {isLoading ? <div>Loading events...</div> : null}
41
- {!isLoading && !error && displayData.length === 0 && (
219
+ {!isLoading && !error && sortedEvents.length === 0 && (
42
220
  <div className="text-sm">No events found</div>
43
221
  )}
44
- {displayData.length > 0 && !error ? (
222
+ {sortedEvents.length > 0 && !error ? (
45
223
  <div className="flex flex-col gap-2">
46
- {displayData.map((event, index) => (
47
- <DetailCard
48
- key={`${event.name}-${index}`}
49
- summary={
50
- <>
51
- <span
52
- className="font-medium"
53
- style={{ color: 'var(--ds-gray-1000)' }}
54
- >
55
- {event.name}
56
- </span>{' '}
57
- -{' '}
58
- <span style={{ color: 'var(--ds-gray-700)' }}>
59
- {localMillisecondTime(
60
- event.timestamp[0] * 1000 + event.timestamp[1] / 1e6
61
- )}
62
- </span>
63
- </>
64
- }
65
- >
66
- {/* Bordered container with separator */}
67
- <div
68
- className="flex flex-col divide-y rounded-md border overflow-hidden"
69
- style={{
70
- borderColor: 'var(--ds-gray-300)',
71
- backgroundColor: 'var(--ds-gray-100)',
72
- }}
73
- >
74
- {Object.entries(event.attributes)
75
- .filter(([key]) => key !== 'eventData')
76
- .map(([key, value]) => (
77
- <div
78
- key={key}
79
- className="flex items-center justify-between px-2.5 py-1.5"
80
- style={{ borderColor: 'var(--ds-gray-300)' }}
81
- >
82
- <span
83
- className="text-[11px] font-medium"
84
- style={{ color: 'var(--ds-gray-700)' }}
85
- >
86
- {key}
87
- </span>
88
- <span
89
- className="text-[11px] font-mono"
90
- style={{ color: 'var(--ds-gray-1000)' }}
91
- >
92
- {String(value)}
93
- </span>
94
- </div>
95
- ))}
96
- </div>
97
- {error ? (
98
- <ErrorCard
99
- title="Failed to load event data"
100
- details={String(error)}
101
- className="my-4"
102
- />
103
- ) : null}
104
- {!error && !isLoading && event.attributes.eventData != null && (
105
- <div className="mt-2">
106
- <AttributeBlock
107
- isLoading={isLoading}
108
- attribute="eventData"
109
- value={event.attributes.eventData}
110
- />
111
- </div>
112
- )}
113
- </DetailCard>
224
+ {sortedEvents.map((event) => (
225
+ <EventItem
226
+ key={event.eventId}
227
+ event={event}
228
+ onLoadEventData={onLoadEventData}
229
+ />
114
230
  ))}
115
231
  </div>
116
232
  ) : null}
@@ -1,23 +1,28 @@
1
1
  'use client';
2
2
 
3
3
  import { useCallback, useEffect, useRef, useState } from 'react';
4
+ import { ObjectInspector } from 'react-inspector';
5
+ import { useDarkMode } from '../hooks/use-dark-mode';
6
+
7
+ export interface StreamChunk {
8
+ id: number;
9
+ /** The raw hydrated value from the stream */
10
+ data: unknown;
11
+ }
4
12
 
5
13
  interface StreamViewerProps {
6
14
  streamId: string;
7
- chunks: Chunk[];
15
+ chunks: StreamChunk[];
8
16
  isLive: boolean;
9
17
  error?: string | null;
10
18
  }
11
19
 
12
- interface Chunk {
13
- id: number;
14
- text: string;
15
- }
20
+ import { inspectorThemeDark, inspectorThemeLight } from './ui/inspector-theme';
16
21
 
17
22
  /**
18
23
  * StreamViewer component that displays real-time stream data.
19
- * It connects to a stream and displays chunks as they arrive,
20
- * with auto-scroll functionality.
24
+ * Each chunk is rendered with ObjectInspector for proper display
25
+ * of complex types (Map, Set, Date, custom classes, etc.).
21
26
  */
22
27
  export function StreamViewer({
23
28
  streamId,
@@ -25,7 +30,7 @@ export function StreamViewer({
25
30
  isLive,
26
31
  error,
27
32
  }: StreamViewerProps) {
28
- // TODO: Handle 410 error specifically (stream expired)
33
+ const isDark = useDarkMode();
29
34
  const [hasMoreBelow, setHasMoreBelow] = useState(false);
30
35
  const scrollRef = useRef<HTMLDivElement>(null);
31
36
 
@@ -39,14 +44,14 @@ export function StreamViewer({
39
44
 
40
45
  // biome-ignore lint/correctness/useExhaustiveDependencies: chunks.length triggers scroll on new chunks
41
46
  useEffect(() => {
42
- // Auto-scroll to bottom when new content arrives
43
47
  if (scrollRef.current) {
44
48
  scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
45
49
  }
46
- // Check scroll position after content changes
47
50
  checkScrollPosition();
48
51
  }, [chunks.length, checkScrollPosition]);
49
52
 
53
+ const theme = isDark ? inspectorThemeDark : inspectorThemeLight;
54
+
50
55
  return (
51
56
  <div className="flex flex-col h-full pb-4">
52
57
  <div className="flex items-center justify-between mb-3 px-1">
@@ -106,25 +111,36 @@ export function StreamViewer({
106
111
  </div>
107
112
  ) : (
108
113
  chunks.map((chunk, index) => (
109
- <pre
114
+ <div
110
115
  key={`${streamId}-chunk-${chunk.id}`}
111
- className="text-[11px] rounded-md border p-3 m-0 whitespace-pre-wrap break-words"
116
+ className="rounded-md border p-3"
112
117
  style={{
113
118
  borderColor: 'var(--ds-gray-300)',
114
- backgroundColor: 'var(--ds-gray-100)',
115
- color: 'var(--ds-gray-1000)',
116
119
  }}
117
120
  >
118
- <code>
121
+ <span
122
+ className="select-none mr-2 text-[11px] font-mono"
123
+ style={{ color: 'var(--ds-gray-500)' }}
124
+ >
125
+ [{index}]
126
+ </span>
127
+ {typeof chunk.data === 'string' ? (
119
128
  <span
120
- className="select-none mr-2"
121
- style={{ color: 'var(--ds-gray-500)' }}
129
+ className="text-[11px] font-mono"
130
+ style={{ color: 'var(--ds-gray-1000)' }}
122
131
  >
123
- [{index}]
132
+ {chunk.data}
124
133
  </span>
125
- {chunk.text}
126
- </code>
127
- </pre>
134
+ ) : (
135
+ <ObjectInspector
136
+ data={chunk.data}
137
+ // @ts-expect-error react-inspector accepts theme objects at runtime despite
138
+ // types declaring string only — see https://github.com/storybookjs/react-inspector/blob/main/README.md#theme
139
+ theme={theme}
140
+ expandLevel={1}
141
+ />
142
+ )}
143
+ </div>
128
144
  ))
129
145
  )}
130
146
  </div>
@@ -118,8 +118,9 @@ export interface TraceViewerState {
118
118
  */
119
119
  isMobile: boolean;
120
120
  /**
121
- * Panel to render instead of the default span detail panel. The panel
122
- * should use the context to get the selected span and other state.
121
+ * @deprecated Panel rendering has been moved outside the context.
122
+ * This field is kept for backwards compatibility but is no longer
123
+ * used by the workflow trace viewer.
123
124
  */
124
125
  customPanelComponent: ReactNode | null;
125
126
  /**
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Shared theme configuration for react-inspector's ObjectInspector.
3
+ *
4
+ * Used by AttributePanel, EventListView, StreamViewer, and any other
5
+ * component that renders data with ObjectInspector.
6
+ */
7
+
8
+ export const inspectorThemeLight = {
9
+ BASE_FONT_SIZE: '11px',
10
+ BASE_LINE_HEIGHT: 1.4,
11
+ BASE_BACKGROUND_COLOR: 'transparent',
12
+ BASE_COLOR: 'var(--ds-gray-1000)',
13
+ OBJECT_PREVIEW_ARRAY_MAX_PROPERTIES: 10,
14
+ OBJECT_PREVIEW_OBJECT_MAX_PROPERTIES: 5,
15
+ OBJECT_NAME_COLOR: 'rgb(136, 19, 145)',
16
+ OBJECT_VALUE_NULL_COLOR: 'rgb(128, 128, 128)',
17
+ OBJECT_VALUE_UNDEFINED_COLOR: 'rgb(128, 128, 128)',
18
+ OBJECT_VALUE_REGEXP_COLOR: 'rgb(196, 26, 22)',
19
+ OBJECT_VALUE_STRING_COLOR: 'rgb(196, 26, 22)',
20
+ OBJECT_VALUE_SYMBOL_COLOR: 'rgb(196, 26, 22)',
21
+ OBJECT_VALUE_NUMBER_COLOR: 'rgb(28, 0, 207)',
22
+ OBJECT_VALUE_BOOLEAN_COLOR: 'rgb(28, 0, 207)',
23
+ OBJECT_VALUE_FUNCTION_PREFIX_COLOR: 'rgb(13, 34, 170)',
24
+ HTML_TAG_COLOR: 'rgb(168, 148, 166)',
25
+ HTML_TAGNAME_COLOR: 'rgb(136, 18, 128)',
26
+ HTML_TAGNAME_TEXT_TRANSFORM: 'lowercase',
27
+ HTML_ATTRIBUTE_NAME_COLOR: 'rgb(153, 69, 0)',
28
+ HTML_ATTRIBUTE_VALUE_COLOR: 'rgb(26, 26, 166)',
29
+ HTML_COMMENT_COLOR: 'rgb(35, 110, 37)',
30
+ HTML_DOCTYPE_COLOR: 'rgb(192, 192, 192)',
31
+ ARROW_COLOR: 'var(--ds-gray-600)',
32
+ ARROW_MARGIN_RIGHT: 3,
33
+ ARROW_FONT_SIZE: 12,
34
+ TREENODE_FONT_FAMILY: 'var(--font-mono)',
35
+ TREENODE_FONT_SIZE: '11px',
36
+ TREENODE_LINE_HEIGHT: 1.4,
37
+ TREENODE_PADDING_LEFT: 12,
38
+ TABLE_BORDER_COLOR: 'var(--ds-gray-300)',
39
+ TABLE_TH_BACKGROUND_COLOR: 'var(--ds-gray-100)',
40
+ TABLE_TH_HOVER_COLOR: 'var(--ds-gray-200)',
41
+ TABLE_SORT_ICON_COLOR: 'var(--ds-gray-500)',
42
+ TABLE_DATA_BACKGROUND_IMAGE: 'none',
43
+ TABLE_DATA_BACKGROUND_SIZE: '0',
44
+ };
45
+
46
+ export const inspectorThemeDark = {
47
+ ...inspectorThemeLight,
48
+ BASE_COLOR: 'var(--ds-gray-1000)',
49
+ OBJECT_NAME_COLOR: 'rgb(227, 110, 236)',
50
+ OBJECT_VALUE_NULL_COLOR: 'rgb(127, 127, 127)',
51
+ OBJECT_VALUE_UNDEFINED_COLOR: 'rgb(127, 127, 127)',
52
+ OBJECT_VALUE_REGEXP_COLOR: 'rgb(233, 63, 59)',
53
+ OBJECT_VALUE_STRING_COLOR: 'rgb(233, 63, 59)',
54
+ OBJECT_VALUE_SYMBOL_COLOR: 'rgb(233, 63, 59)',
55
+ OBJECT_VALUE_NUMBER_COLOR: 'hsl(252, 100%, 75%)',
56
+ OBJECT_VALUE_BOOLEAN_COLOR: 'hsl(252, 100%, 75%)',
57
+ OBJECT_VALUE_FUNCTION_PREFIX_COLOR: 'rgb(85, 106, 242)',
58
+ HTML_TAG_COLOR: 'rgb(93, 176, 215)',
59
+ HTML_TAGNAME_COLOR: 'rgb(93, 176, 215)',
60
+ HTML_ATTRIBUTE_NAME_COLOR: 'rgb(155, 187, 220)',
61
+ HTML_ATTRIBUTE_VALUE_COLOR: 'rgb(242, 151, 102)',
62
+ HTML_COMMENT_COLOR: 'rgb(137, 137, 137)',
63
+ HTML_DOCTYPE_COLOR: 'rgb(192, 192, 192)',
64
+ };