@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
@@ -5,9 +5,10 @@ import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
5
5
  import type { ModelMessage } from 'ai';
6
6
  import type { ReactNode } from 'react';
7
7
  import { createContext, useContext, useMemo, useState } from 'react';
8
- import { ErrorCard } from '../ui/error-card';
8
+ import { ObjectInspector } from 'react-inspector';
9
9
  import { useDarkMode } from '../../hooks/use-dark-mode';
10
10
  import { extractConversation, isDoStreamStep } from '../../lib/utils';
11
+ import { ErrorCard } from '../ui/error-card';
11
12
  import { ConversationView } from './conversation-view';
12
13
  import { DetailCard } from './detail-card';
13
14
 
@@ -334,145 +335,45 @@ const StreamRefDisplay = ({ streamRef }: { streamRef: StreamRef }) => {
334
335
  };
335
336
 
336
337
  /**
337
- * Recursively transforms a value for JSON display, replacing StreamRef and
338
- * ClassInstanceRef objects with placeholder strings that can be identified
339
- * and replaced with React elements.
338
+ * Renders a value using react-inspector's ObjectInspector for proper
339
+ * display of Map, Set, URLSearchParams, Date, Error, RegExp, typed
340
+ * arrays, and other non-plain-object types.
341
+ *
342
+ * StreamRef and ClassInstanceRef objects are rendered inline as
343
+ * custom components (clickable stream links and class cards).
340
344
  */
341
- const transformValueForDisplay = (
342
- value: unknown
343
- ): {
344
- json: string;
345
- streamRefs: Map<string, StreamRef>;
346
- classInstanceRefs: Map<string, ClassInstanceRef>;
347
- } => {
348
- const streamRefs = new Map<string, StreamRef>();
349
- const classInstanceRefs = new Map<string, ClassInstanceRef>();
350
- let counter = 0;
351
-
352
- const transform = (v: unknown): unknown => {
353
- if (isStreamRef(v)) {
354
- const placeholder = `__STREAM_REF_${counter++}__`;
355
- streamRefs.set(placeholder, v);
356
- return placeholder;
357
- }
358
- if (isClassInstanceRef(v)) {
359
- const placeholder = `__CLASS_INSTANCE_REF_${counter++}__`;
360
- classInstanceRefs.set(placeholder, v);
361
- return placeholder;
362
- }
363
- if (Array.isArray(v)) {
364
- return v.map(transform);
365
- }
366
- if (v !== null && typeof v === 'object') {
367
- const result: Record<string, unknown> = {};
368
- for (const [key, val] of Object.entries(v)) {
369
- result[key] = transform(val);
370
- }
371
- return result;
372
- }
373
- return v;
374
- };
375
-
376
- const transformed = transform(value);
377
- return {
378
- json: JSON.stringify(transformed, null, 2),
379
- streamRefs,
380
- classInstanceRefs,
381
- };
382
- };
383
-
384
345
  const JsonBlock = (value: unknown) => {
385
- const { json, streamRefs, classInstanceRefs } =
386
- transformValueForDisplay(value);
387
-
388
- // If no special refs, just render plain JSON
389
- if (streamRefs.size === 0 && classInstanceRefs.size === 0) {
390
- return (
391
- <pre
392
- className="text-[11px] overflow-x-auto rounded-md border p-3"
393
- style={{
394
- borderColor: 'var(--ds-gray-300)',
395
- backgroundColor: 'var(--ds-gray-100)',
396
- color: 'var(--ds-gray-1000)',
397
- }}
398
- >
399
- <code>{json}</code>
400
- </pre>
401
- );
402
- }
346
+ return <DataInspector data={value} />;
347
+ };
403
348
 
404
- // Build a combined map of all placeholders to their React elements
405
- const placeholderComponents = new Map<string, ReactNode>();
406
- let keyIndex = 0;
349
+ import { inspectorThemeDark, inspectorThemeLight } from '../ui/inspector-theme';
407
350
 
408
- for (const [placeholder, streamRef] of streamRefs) {
409
- placeholderComponents.set(
410
- placeholder,
411
- <StreamRefDisplay key={keyIndex++} streamRef={streamRef} />
412
- );
413
- }
351
+ function DataInspector({ data }: { data: unknown }) {
352
+ const isDark = useDarkMode();
414
353
 
415
- for (const [placeholder, classInstanceRef] of classInstanceRefs) {
416
- placeholderComponents.set(
417
- placeholder,
418
- <ClassInstanceRefDisplay
419
- key={keyIndex++}
420
- classInstanceRef={classInstanceRef}
421
- />
422
- );
354
+ // Render top-level StreamRef/ClassInstanceRef as full custom components
355
+ if (isStreamRef(data)) {
356
+ return <StreamRefDisplay streamRef={data} />;
423
357
  }
424
-
425
- // Split the JSON by all placeholders and render with React elements
426
- const parts: ReactNode[] = [];
427
- let remaining = json;
428
-
429
- // Process placeholders in order of their appearance in the string
430
- while (remaining.length > 0) {
431
- let earliestIndex = -1;
432
- let earliestPlaceholder = '';
433
- let earliestComponent: ReactNode = null;
434
-
435
- // Find the earliest placeholder in the remaining string
436
- for (const [placeholder, component] of placeholderComponents) {
437
- const index = remaining.indexOf(`"${placeholder}"`);
438
- if (index !== -1 && (earliestIndex === -1 || index < earliestIndex)) {
439
- earliestIndex = index;
440
- earliestPlaceholder = placeholder;
441
- earliestComponent = component;
442
- }
443
- }
444
-
445
- if (earliestIndex === -1) {
446
- // No more placeholders found, add the rest
447
- parts.push(remaining);
448
- break;
449
- }
450
-
451
- // Add text before the placeholder
452
- if (earliestIndex > 0) {
453
- parts.push(remaining.slice(0, earliestIndex));
454
- }
455
-
456
- // Add the component
457
- parts.push(earliestComponent);
458
-
459
- // Move past the placeholder
460
- remaining = remaining.slice(earliestIndex + earliestPlaceholder.length + 2); // +2 for quotes
358
+ if (isClassInstanceRef(data)) {
359
+ return <ClassInstanceRefDisplay classInstanceRef={data} />;
461
360
  }
462
361
 
463
362
  return (
464
- <pre
465
- className="text-[11px] overflow-x-auto rounded-md border p-3"
466
- style={{
467
- borderColor: 'var(--ds-gray-300)',
468
- backgroundColor: 'var(--ds-gray-100)',
469
- color: 'var(--ds-gray-1000)',
470
- }}
363
+ <div
364
+ className="overflow-x-auto rounded-md border p-3"
365
+ style={{ borderColor: 'var(--ds-gray-300)' }}
471
366
  >
472
- <code>{parts}</code>
473
- </pre>
367
+ <ObjectInspector
368
+ data={data}
369
+ // @ts-expect-error react-inspector accepts theme objects at runtime despite
370
+ // types declaring string only — see https://github.com/storybookjs/react-inspector/blob/main/README.md#theme
371
+ theme={isDark ? inspectorThemeDark : inspectorThemeLight}
372
+ expandLevel={2}
373
+ />
374
+ </div>
474
375
  );
475
- };
376
+ }
476
377
 
477
378
  type AttributeKey =
478
379
  | keyof Step
@@ -3,12 +3,18 @@ import type { ReactNode } from 'react';
3
3
  export function DetailCard({
4
4
  summary,
5
5
  children,
6
+ onToggle,
6
7
  }: {
7
8
  summary: ReactNode;
8
9
  children?: ReactNode;
10
+ /** Called when the detail card is expanded/collapsed */
11
+ onToggle?: (open: boolean) => void;
9
12
  }) {
10
13
  return (
11
- <details className="group">
14
+ <details
15
+ className="group"
16
+ onToggle={(e) => onToggle?.((e.target as HTMLDetailsElement).open)}
17
+ >
12
18
  <summary
13
19
  className="cursor-pointer rounded-md border px-2.5 py-1.5 text-xs hover:brightness-95"
14
20
  style={{
@@ -3,9 +3,8 @@
3
3
  import type { Event, Hook, Step, 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 { toast } from 'sonner';
8
- import { useTraceViewer } from '../trace-viewer';
9
8
  import { AttributePanel } from './attribute-panel';
10
9
  import { EventsList } from './events-list';
11
10
  import { ResolveHookModal } from './resolve-hook-modal';
@@ -33,7 +32,24 @@ export type SpanSelectionInfo = {
33
32
  };
34
33
 
35
34
  /**
36
- * Custom panel component for workflow traces that displays entity details
35
+ * Info about the selected span from the trace viewer.
36
+ */
37
+ export interface SelectedSpanInfo {
38
+ /** The raw data from the span attributes (step/run/hook object from the trace) */
39
+ data?: unknown;
40
+ /** The span resource type (from span attributes) */
41
+ resource?: string;
42
+ /** The span ID (correlationId for filtering events) */
43
+ spanId?: string;
44
+ /** Raw correlated events from the store (NOT from the trace worker pipeline) */
45
+ rawEvents?: Event[];
46
+ }
47
+
48
+ /**
49
+ * Panel component for workflow traces that displays entity details.
50
+ *
51
+ * This component is rendered OUTSIDE the trace viewer context — it
52
+ * receives all data via props rather than reading from context.
37
53
  */
38
54
  export function EntityDetailPanel({
39
55
  run,
@@ -43,7 +59,9 @@ export function EntityDetailPanel({
43
59
  spanDetailLoading,
44
60
  onSpanSelect,
45
61
  onWakeUpSleep,
62
+ onLoadEventData,
46
63
  onResolveHook,
64
+ selectedSpan,
47
65
  }: {
48
66
  run: WorkflowRun;
49
67
  /** Callback when a stream reference is clicked */
@@ -61,47 +79,53 @@ export function EntityDetailPanel({
61
79
  runId: string,
62
80
  correlationId: string
63
81
  ) => Promise<{ stoppedCount: number }>;
82
+ /** Callback to load event data for a specific event (lazy loading) */
83
+ onLoadEventData?: (
84
+ correlationId: string,
85
+ eventId: string
86
+ ) => Promise<unknown | null>;
64
87
  /** Callback to resolve a hook with a payload. */
65
88
  onResolveHook?: (
66
89
  hookToken: string,
67
90
  payload: unknown,
68
91
  hook?: Hook
69
92
  ) => Promise<void>;
93
+ /** Info about the currently selected span from the trace viewer */
94
+ selectedSpan: SelectedSpanInfo | null;
70
95
  }): React.JSX.Element | null {
71
- const { state } = useTraceViewer();
72
- const { selected } = state;
73
96
  const [stoppingSleep, setStoppingSleep] = useState(false);
74
97
  const [showResolveHookModal, setShowResolveHookModal] = useState(false);
75
98
  const [resolvingHook, setResolvingHook] = useState(false);
76
99
 
77
- const data = selected?.span.attributes?.data;
78
-
79
- // Stable ref for onSpanSelect to avoid re-render loops when parent
80
- // doesn't memoize the callback with useCallback.
81
- const onSpanSelectRef = useRef(onSpanSelect);
82
- useEffect(() => {
83
- onSpanSelectRef.current = onSpanSelect;
84
- });
100
+ const data = selectedSpan?.data;
101
+ const rawEvents = selectedSpan?.rawEvents;
102
+ const rawEventsLength = rawEvents?.length ?? 0;
85
103
 
86
- // Determine resource ID and runId (needed for steps)
87
- // Uses type guards to validate the data shape matches the expected resource type
104
+ // Determine resource type, ID, and runId from the selected span
88
105
  const { resource, resourceId, runId } = useMemo(() => {
89
- const resource = selected?.span.attributes?.resource;
90
- if (resource === 'step' && isStep(data)) {
106
+ if (!selectedSpan) {
107
+ return { resource: undefined, resourceId: undefined, runId: undefined };
108
+ }
109
+
110
+ const res = selectedSpan.resource;
111
+ if (res === 'step' && isStep(data)) {
91
112
  return { resource: 'step', resourceId: data.stepId, runId: data.runId };
92
- } else if (resource === 'run' && isWorkflowRun(data)) {
113
+ }
114
+ if (res === 'run' && isWorkflowRun(data)) {
93
115
  return { resource: 'run', resourceId: data.runId, runId: undefined };
94
- } else if (resource === 'hook' && isHook(data)) {
116
+ }
117
+ if (res === 'hook' && isHook(data)) {
95
118
  return { resource: 'hook', resourceId: data.hookId, runId: undefined };
96
- } else if (resource === 'sleep') {
119
+ }
120
+ if (res === 'sleep') {
97
121
  return {
98
122
  resource: 'sleep',
99
- resourceId: selected?.span?.spanId,
123
+ resourceId: selectedSpan.spanId,
100
124
  runId: undefined,
101
125
  };
102
126
  }
103
127
  return { resource: undefined, resourceId: undefined, runId: undefined };
104
- }, [selected, data]);
128
+ }, [selectedSpan, data]);
105
129
 
106
130
  // Notify parent when span selection changes
107
131
  useEffect(() => {
@@ -110,65 +134,53 @@ export function EntityDetailPanel({
110
134
  resourceId &&
111
135
  ['run', 'step', 'hook', 'sleep'].includes(resource)
112
136
  ) {
113
- onSpanSelectRef.current({
137
+ onSpanSelect({
114
138
  resource: resource as 'run' | 'step' | 'hook' | 'sleep',
115
139
  resourceId,
116
140
  runId,
117
141
  });
118
142
  }
119
- }, [resource, resourceId, runId]);
143
+ }, [resource, resourceId, runId, onSpanSelect]);
120
144
 
121
145
  // Check if this sleep is still pending and can be woken up
122
- // Requirements: no wait_completed event, resumeAt is in the future, run is not terminal
123
- const spanEvents = selected?.span.events;
124
- const spanEventsLength = spanEvents?.length ?? 0;
125
146
  const canWakeUp = useMemo(() => {
126
- void spanEventsLength; // Force dependency on length for reactivity
127
- if (resource !== 'sleep' || !spanEvents) return false;
128
-
129
- // Check run is not in a terminal state
147
+ void rawEventsLength;
148
+ if (resource !== 'sleep' || !rawEvents) return false;
130
149
  const terminalStates = ['completed', 'failed', 'cancelled'];
131
150
  if (terminalStates.includes(run.status)) return false;
132
-
133
- // Check if wait has already completed
134
- const hasWaitCompleted = spanEvents.some(
135
- (e) => e.name === 'wait_completed'
151
+ const hasWaitCompleted = rawEvents.some(
152
+ (e) => e.eventType === 'wait_completed'
136
153
  );
137
154
  if (hasWaitCompleted) return false;
138
-
139
- // Check if resumeAt is in the future
140
- const waitCreatedEvent = spanEvents.find((e) => e.name === 'wait_created');
141
- const eventData = waitCreatedEvent?.attributes?.eventData as
155
+ const waitCreatedEvent = rawEvents.find(
156
+ (e) => e.eventType === 'wait_created'
157
+ );
158
+ const eventData = (waitCreatedEvent as any)?.eventData as
142
159
  | { resumeAt?: string | Date }
143
160
  | undefined;
144
161
  const resumeAt = eventData?.resumeAt;
145
162
  if (!resumeAt) return false;
146
-
147
163
  const resumeAtDate = new Date(resumeAt);
148
164
  return resumeAtDate.getTime() > Date.now();
149
- }, [resource, spanEvents, spanEventsLength, run.status]);
165
+ }, [resource, rawEvents, rawEventsLength, run.status]);
150
166
 
151
- // Check if this hook can be resolved (not yet resolved, run is not terminal)
167
+ // Check if this hook can be resolved
152
168
  const canResolveHook = useMemo(() => {
153
- void spanEventsLength; // Force dependency on length for reactivity
154
- if (resource !== 'hook' || !spanEvents) return false;
155
-
156
- // Check run is not in a terminal state
169
+ void rawEventsLength;
170
+ if (resource !== 'hook' || !rawEvents) return false;
157
171
  const terminalStates = ['completed', 'failed', 'cancelled'];
158
172
  if (terminalStates.includes(run.status)) return false;
159
-
160
- // Check if hook has already been disposed
161
- const hasHookDisposed = spanEvents.some((e) => e.name === 'hook_disposed');
173
+ const hasHookDisposed = rawEvents.some(
174
+ (e) => e.eventType === 'hook_disposed'
175
+ );
162
176
  if (hasHookDisposed) return false;
163
-
164
- // Hook can be resolved
165
177
  return true;
166
- }, [resource, spanEvents, spanEventsLength, run.status]);
178
+ }, [resource, rawEvents, rawEventsLength, run.status]);
167
179
 
168
180
  const error = spanDetailError ?? undefined;
169
181
  const loading = spanDetailLoading ?? false;
170
182
 
171
- // Get the hook token for resolving (prefer fetched data when available)
183
+ // Get the hook token for resolving
172
184
  const hookToken = useMemo(() => {
173
185
  if (resource !== 'hook') return undefined;
174
186
  const candidate = spanDetailData ?? data;
@@ -176,12 +188,12 @@ export function EntityDetailPanel({
176
188
  }, [resource, spanDetailData, data]);
177
189
 
178
190
  useEffect(() => {
179
- if (error && selected && resource) {
191
+ if (error && selectedSpan && resource) {
180
192
  toast.error(`Failed to load ${resource} details`, {
181
193
  description: error.message,
182
194
  });
183
195
  }
184
- }, [error, resource, selected]);
196
+ }, [error, resource, selectedSpan]);
185
197
 
186
198
  const handleWakeUp = async () => {
187
199
  if (stoppingSleep || !resourceId) return;
@@ -255,7 +267,7 @@ export function EntityDetailPanel({
255
267
  [onResolveHook, hookToken, resolvingHook, spanDetailData, data]
256
268
  );
257
269
 
258
- if (!selected || !resource || !resourceId) {
270
+ if (!selectedSpan || !resource || !resourceId) {
259
271
  return null;
260
272
  }
261
273
 
@@ -332,7 +344,9 @@ export function EntityDetailPanel({
332
344
  error={error ?? undefined}
333
345
  onStreamClick={onStreamClick}
334
346
  />
335
- {resource !== 'run' && <EventsList events={selected.span.events} />}
347
+ {resource !== 'run' && rawEvents && (
348
+ <EventsList events={rawEvents} onLoadEventData={onLoadEventData} />
349
+ )}
336
350
  </div>
337
351
  );
338
352
  }