@workflow/web-shared 5.0.0-beta.1 → 5.0.0-beta.2

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 (102) hide show
  1. package/README.md +1 -1
  2. package/dist/components/error-boundary.js.map +1 -1
  3. package/dist/components/event-list-view.d.ts +3 -1
  4. package/dist/components/event-list-view.d.ts.map +1 -1
  5. package/dist/components/event-list-view.js +53 -53
  6. package/dist/components/event-list-view.js.map +1 -1
  7. package/dist/components/hook-actions.js.map +1 -1
  8. package/dist/components/index.d.ts +1 -1
  9. package/dist/components/index.d.ts.map +1 -1
  10. package/dist/components/index.js +1 -1
  11. package/dist/components/index.js.map +1 -1
  12. package/dist/components/run-trace-view.d.ts +2 -1
  13. package/dist/components/run-trace-view.d.ts.map +1 -1
  14. package/dist/components/run-trace-view.js +2 -2
  15. package/dist/components/run-trace-view.js.map +1 -1
  16. package/dist/components/sidebar/attribute-panel.d.ts +7 -1
  17. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  18. package/dist/components/sidebar/attribute-panel.js +36 -26
  19. package/dist/components/sidebar/attribute-panel.js.map +1 -1
  20. package/dist/components/sidebar/conversation-view.js.map +1 -1
  21. package/dist/components/sidebar/copyable-data-block.js.map +1 -1
  22. package/dist/components/sidebar/detail-card.js.map +1 -1
  23. package/dist/components/sidebar/entity-detail-panel.d.ts +5 -1
  24. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  25. package/dist/components/sidebar/entity-detail-panel.js +18 -16
  26. package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
  27. package/dist/components/sidebar/events-list.js.map +1 -1
  28. package/dist/components/sidebar/resolve-hook-modal.js.map +1 -1
  29. package/dist/components/stream-viewer.js.map +1 -1
  30. package/dist/components/trace-viewer/components/map.js.map +1 -1
  31. package/dist/components/trace-viewer/components/markers.js.map +1 -1
  32. package/dist/components/trace-viewer/components/node.js.map +1 -1
  33. package/dist/components/trace-viewer/components/search-input.js.map +1 -1
  34. package/dist/components/trace-viewer/components/search.js.map +1 -1
  35. package/dist/components/trace-viewer/components/span-content.js.map +1 -1
  36. package/dist/components/trace-viewer/components/span-detail-panel.js.map +1 -1
  37. package/dist/components/trace-viewer/components/span-segments.js.map +1 -1
  38. package/dist/components/trace-viewer/components/span-strategies.js.map +1 -1
  39. package/dist/components/trace-viewer/components/ui.js.map +1 -1
  40. package/dist/components/trace-viewer/components/zoom-button.js.map +1 -1
  41. package/dist/components/trace-viewer/components/zoom-icons.js.map +1 -1
  42. package/dist/components/trace-viewer/context.js.map +1 -1
  43. package/dist/components/trace-viewer/index.js.map +1 -1
  44. package/dist/components/trace-viewer/trace-viewer.js.map +1 -1
  45. package/dist/components/trace-viewer/types.js.map +1 -1
  46. package/dist/components/trace-viewer/util/constants.js.map +1 -1
  47. package/dist/components/trace-viewer/util/scrollbar-width.js.map +1 -1
  48. package/dist/components/trace-viewer/util/timing.js.map +1 -1
  49. package/dist/components/trace-viewer/util/tree.js.map +1 -1
  50. package/dist/components/trace-viewer/util/use-immediate-style.js.map +1 -1
  51. package/dist/components/trace-viewer/util/use-streaming-spans.js.map +1 -1
  52. package/dist/components/trace-viewer/util/use-trackpad-zoom.js.map +1 -1
  53. package/dist/components/trace-viewer/worker.js.map +1 -1
  54. package/dist/components/ui/alert.js.map +1 -1
  55. package/dist/components/ui/card.js.map +1 -1
  56. package/dist/components/ui/data-inspector.d.ts +17 -1
  57. package/dist/components/ui/data-inspector.d.ts.map +1 -1
  58. package/dist/components/ui/data-inspector.js +116 -21
  59. package/dist/components/ui/data-inspector.js.map +1 -1
  60. package/dist/components/ui/decrypt-button.js.map +1 -1
  61. package/dist/components/ui/error-card.js.map +1 -1
  62. package/dist/components/ui/error-stack-block.js.map +1 -1
  63. package/dist/components/ui/inspector-theme.js.map +1 -1
  64. package/dist/components/ui/load-more-button.js.map +1 -1
  65. package/dist/components/ui/menu-dropdown.js.map +1 -1
  66. package/dist/components/ui/skeleton.js.map +1 -1
  67. package/dist/components/ui/spinner.js.map +1 -1
  68. package/dist/components/ui/timestamp-tooltip.js.map +1 -1
  69. package/dist/components/workflow-trace-view.d.ts +5 -1
  70. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  71. package/dist/components/workflow-trace-view.js +7 -2
  72. package/dist/components/workflow-trace-view.js.map +1 -1
  73. package/dist/components/workflow-traces/event-colors.js.map +1 -1
  74. package/dist/components/workflow-traces/trace-colors.js.map +1 -1
  75. package/dist/components/workflow-traces/trace-span-construction.js +1 -1
  76. package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
  77. package/dist/components/workflow-traces/trace-time-utils.js.map +1 -1
  78. package/dist/hooks/use-dark-mode.js.map +1 -1
  79. package/dist/index.d.ts +1 -1
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +1 -1
  82. package/dist/index.js.map +1 -1
  83. package/dist/lib/event-analysis.js.map +1 -1
  84. package/dist/lib/event-materialization.js.map +1 -1
  85. package/dist/lib/hydration.d.ts +8 -1
  86. package/dist/lib/hydration.d.ts.map +1 -1
  87. package/dist/lib/hydration.js +32 -2
  88. package/dist/lib/hydration.js.map +1 -1
  89. package/dist/lib/toast.js.map +1 -1
  90. package/dist/lib/trace-builder.js.map +1 -1
  91. package/dist/lib/utils.js.map +1 -1
  92. package/package.json +3 -3
  93. package/src/components/event-list-view.tsx +181 -173
  94. package/src/components/index.ts +6 -1
  95. package/src/components/run-trace-view.tsx +3 -0
  96. package/src/components/sidebar/attribute-panel.tsx +160 -109
  97. package/src/components/sidebar/entity-detail-panel.tsx +111 -96
  98. package/src/components/ui/data-inspector.tsx +205 -31
  99. package/src/components/workflow-trace-view.tsx +14 -0
  100. package/src/components/workflow-traces/trace-span-construction.ts +1 -1
  101. package/src/index.ts +1 -0
  102. package/src/lib/hydration.ts +37 -0
@@ -9,7 +9,14 @@
9
9
  */
10
10
 
11
11
  import { Lock } from 'lucide-react';
12
- import { createContext, useContext, useEffect, useRef, useState } from 'react';
12
+ import {
13
+ createContext,
14
+ useContext,
15
+ useEffect,
16
+ useMemo,
17
+ useRef,
18
+ useState,
19
+ } from 'react';
13
20
  import {
14
21
  ObjectInspector,
15
22
  ObjectLabel,
@@ -26,6 +33,7 @@ import {
26
33
  inspectorThemeExtendedLight,
27
34
  inspectorThemeLight,
28
35
  } from './inspector-theme';
36
+ import { Spinner } from './spinner';
29
37
 
30
38
  // ---------------------------------------------------------------------------
31
39
  // StreamRef / ClassInstanceRef type detection
@@ -34,19 +42,29 @@ import {
34
42
 
35
43
  const STREAM_REF_TYPE = '__workflow_stream_ref__';
36
44
  const CLASS_INSTANCE_REF_TYPE = '__workflow_class_instance_ref__';
45
+ const RUN_REF_TYPE = '__workflow_run_ref__';
37
46
 
38
47
  interface StreamRef {
39
48
  __type: typeof STREAM_REF_TYPE;
40
49
  streamId: string;
41
50
  }
42
51
 
52
+ interface RunRef {
53
+ __type: typeof RUN_REF_TYPE;
54
+ runId: string;
55
+ }
56
+
43
57
  function isStreamRef(value: unknown): value is StreamRef {
44
- return (
45
- value !== null &&
46
- typeof value === 'object' &&
47
- '__type' in value &&
48
- (value as Record<string, unknown>).__type === STREAM_REF_TYPE
49
- );
58
+ if (value === null || typeof value !== 'object') return false;
59
+ // Check both enumerable and non-enumerable __type (opaque refs use non-enumerable)
60
+ const desc = Object.getOwnPropertyDescriptor(value, '__type');
61
+ return desc?.value === STREAM_REF_TYPE;
62
+ }
63
+
64
+ function isRunRef(value: unknown): value is RunRef {
65
+ if (value === null || typeof value !== 'object') return false;
66
+ const desc = Object.getOwnPropertyDescriptor(value, '__type');
67
+ return desc?.value === RUN_REF_TYPE;
50
68
  }
51
69
 
52
70
  function isClassInstanceRef(value: unknown): value is {
@@ -75,18 +93,89 @@ export const StreamClickContext = createContext<
75
93
  ((streamId: string) => void) | undefined
76
94
  >(undefined);
77
95
 
96
+ /**
97
+ * Context for passing a decrypt handler down to DataInspector instances.
98
+ * When provided, encrypted markers become clickable buttons that trigger decryption.
99
+ */
100
+ export type DecryptClickContextValue = {
101
+ onDecrypt: () => void;
102
+ isDecrypting: boolean;
103
+ };
104
+
105
+ export const DecryptClickContext = createContext<
106
+ DecryptClickContextValue | undefined
107
+ >(undefined);
108
+
109
+ export const RunClickContext = createContext<
110
+ ((runId: string) => void) | undefined
111
+ >(undefined);
112
+
113
+ function EncryptedInlineLabel() {
114
+ const ctx = useContext(DecryptClickContext);
115
+ if (ctx) {
116
+ return (
117
+ <button
118
+ type="button"
119
+ className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[11px] cursor-pointer"
120
+ style={{
121
+ backgroundColor: 'var(--ds-gray-100)',
122
+ color: 'var(--ds-gray-700)',
123
+ border: '1px solid var(--ds-gray-400)',
124
+ fontStyle: 'italic',
125
+ opacity: ctx.isDecrypting ? 0.6 : 1,
126
+ }}
127
+ disabled={ctx.isDecrypting}
128
+ onClick={(e) => {
129
+ e.stopPropagation();
130
+ ctx.onDecrypt();
131
+ }}
132
+ title="Click to decrypt"
133
+ >
134
+ {ctx.isDecrypting ? (
135
+ <Spinner size={12} />
136
+ ) : (
137
+ <Lock
138
+ className="h-3 w-3"
139
+ style={{ display: 'inline', flexShrink: 0 }}
140
+ />
141
+ )}
142
+ <span>{ctx.isDecrypting ? 'Decrypting…' : 'Decrypt'}</span>
143
+ </button>
144
+ );
145
+ }
146
+ return (
147
+ <span style={{ color: 'var(--ds-gray-600)', fontStyle: 'italic' }}>
148
+ <Lock
149
+ className="h-3 w-3"
150
+ style={{
151
+ display: 'inline',
152
+ verticalAlign: 'middle',
153
+ marginRight: '3px',
154
+ marginTop: '-1px',
155
+ }}
156
+ />
157
+ Encrypted
158
+ </span>
159
+ );
160
+ }
78
161
  function StreamRefInline({ streamRef }: { streamRef: StreamRef }) {
79
162
  const onStreamClick = useContext(StreamClickContext);
163
+ const [hovered, setHovered] = useState(false);
80
164
  return (
81
165
  <button
82
166
  type="button"
83
- className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-mono cursor-pointer"
167
+ className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-mono cursor-pointer underline decoration-transparent transition-colors"
84
168
  style={{
85
- backgroundColor: 'var(--ds-blue-100)',
86
- color: 'var(--ds-blue-800)',
169
+ backgroundColor: hovered ? 'var(--ds-blue-200)' : 'var(--ds-blue-100)',
170
+ color: 'var(--ds-blue-900)',
87
171
  border: '1px solid var(--ds-blue-300)',
88
172
  }}
89
- onClick={() => onStreamClick?.(streamRef.streamId)}
173
+ onMouseEnter={() => setHovered(true)}
174
+ onMouseLeave={() => setHovered(false)}
175
+ onClick={(e) => {
176
+ e.stopPropagation();
177
+ onStreamClick?.(streamRef.streamId);
178
+ }}
90
179
  title={`View stream: ${streamRef.streamId}`}
91
180
  >
92
181
  <span>📡</span>
@@ -95,6 +184,33 @@ function StreamRefInline({ streamRef }: { streamRef: StreamRef }) {
95
184
  );
96
185
  }
97
186
 
187
+ function RunRefInline({ runRef }: { runRef: RunRef }) {
188
+ const onRunClick = useContext(RunClickContext);
189
+ const [hovered, setHovered] = useState(false);
190
+ return (
191
+ <button
192
+ type="button"
193
+ className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-mono cursor-pointer underline decoration-transparent transition-colors"
194
+ style={{
195
+ backgroundColor: hovered
196
+ ? 'var(--ds-purple-200)'
197
+ : 'var(--ds-purple-100)',
198
+ color: 'var(--ds-purple-900)',
199
+ border: '1px solid var(--ds-purple-300)',
200
+ }}
201
+ onMouseEnter={() => setHovered(true)}
202
+ onMouseLeave={() => setHovered(false)}
203
+ onClick={(e) => {
204
+ e.stopPropagation();
205
+ onRunClick?.(runRef.runId);
206
+ }}
207
+ title={`View run: ${runRef.runId}`}
208
+ >
209
+ <span>{runRef.runId}</span>
210
+ </button>
211
+ );
212
+ }
213
+
98
214
  // ---------------------------------------------------------------------------
99
215
  // Extended theme context (for colors react-inspector doesn't support natively)
100
216
  // ---------------------------------------------------------------------------
@@ -132,26 +248,13 @@ function NodeRenderer({
132
248
  }) {
133
249
  const extendedTheme = useContext(ExtendedThemeContext);
134
250
 
135
- // Encrypted marker → flat label with Lock icon, non-expandable
251
+ // Encrypted marker → flat label with Lock icon, clickable when onDecrypt is available
136
252
  if (
137
253
  data !== null &&
138
254
  typeof data === 'object' &&
139
255
  data.constructor?.name === ENCRYPTED_DISPLAY_NAME
140
256
  ) {
141
- const label = (
142
- <span style={{ color: 'var(--ds-gray-600)', fontStyle: 'italic' }}>
143
- <Lock
144
- className="h-3 w-3"
145
- style={{
146
- display: 'inline',
147
- verticalAlign: 'middle',
148
- marginRight: '3px',
149
- marginTop: '-1px',
150
- }}
151
- />
152
- Encrypted
153
- </span>
154
- );
257
+ const label = <EncryptedInlineLabel />;
155
258
  if (depth === 0) {
156
259
  return label;
157
260
  }
@@ -175,6 +278,17 @@ function NodeRenderer({
175
278
  );
176
279
  }
177
280
 
281
+ // RunRef → inline clickable badge linking to the target run
282
+ if (isRunRef(data)) {
283
+ return (
284
+ <span>
285
+ {name != null && <ObjectName name={name} />}
286
+ {name != null && <span>: </span>}
287
+ <RunRefInline runRef={data} />
288
+ </span>
289
+ );
290
+ }
291
+
178
292
  // ClassInstanceRef → show className as type, data as the inspectable value
179
293
  if (isClassInstanceRef(data)) {
180
294
  if (depth === 0) {
@@ -225,6 +339,41 @@ function NodeRenderer({
225
339
  // Public component
226
340
  // ---------------------------------------------------------------------------
227
341
 
342
+ /**
343
+ * Create a non-expandable wrapper that carries ref data as non-enumerable
344
+ * properties. ObjectInspector won't render children for objects with no
345
+ * enumerable keys, but our NodeRenderer can still detect them.
346
+ */
347
+ function makeOpaqueRef(ref: Record<string, unknown>): unknown {
348
+ const opaque = Object.create(null);
349
+ for (const [key, value] of Object.entries(ref)) {
350
+ Object.defineProperty(opaque, key, { value, enumerable: false });
351
+ }
352
+ return opaque;
353
+ }
354
+
355
+ /**
356
+ * Recursively walk data and replace RunRef/StreamRef objects with
357
+ * non-expandable versions so ObjectInspector doesn't show their internals.
358
+ * Only recurses into plain objects and arrays to avoid stripping class
359
+ * instances (Date, Error, Map, Set, URL, Headers, etc.) that have their
360
+ * own rendering in NodeRenderer.
361
+ */
362
+ function collapseRefs(data: unknown): unknown {
363
+ if (data === null || typeof data !== 'object') return data;
364
+ if (isRunRef(data) || isStreamRef(data))
365
+ return makeOpaqueRef(data as unknown as Record<string, unknown>);
366
+ if (Array.isArray(data)) return data.map(collapseRefs);
367
+ // Only recurse into plain objects — leave class instances untouched
368
+ const proto = Object.getPrototypeOf(data);
369
+ if (proto !== Object.prototype && proto !== null) return data;
370
+ const result: Record<string, unknown> = {};
371
+ for (const [key, value] of Object.entries(data)) {
372
+ result[key] = collapseRefs(value);
373
+ }
374
+ return result;
375
+ }
376
+
228
377
  export interface DataInspectorProps {
229
378
  /** The data to inspect */
230
379
  data: unknown;
@@ -234,6 +383,12 @@ export interface DataInspectorProps {
234
383
  name?: string;
235
384
  /** Callback when a stream reference is clicked */
236
385
  onStreamClick?: (streamId: string) => void;
386
+ /** Callback when a run reference is clicked */
387
+ onRunClick?: (runId: string) => void;
388
+ /** Callback when an encrypted marker is clicked (triggers decryption) */
389
+ onDecrypt?: () => void;
390
+ /** Whether decryption is currently in progress */
391
+ isDecrypting?: boolean;
237
392
  }
238
393
 
239
394
  export function DataInspector({
@@ -241,8 +396,12 @@ export function DataInspector({
241
396
  expandLevel = 2,
242
397
  name,
243
398
  onStreamClick,
399
+ onRunClick,
400
+ onDecrypt,
401
+ isDecrypting = false,
244
402
  }: DataInspectorProps) {
245
- const stableData = useStableInspectorData(data);
403
+ const collapsedData = useMemo(() => collapseRefs(data), [data]);
404
+ const stableData = useStableInspectorData(collapsedData);
246
405
  const [initialExpandLevel, setInitialExpandLevel] = useState(expandLevel);
247
406
  const isDark = useDarkMode();
248
407
  const extendedTheme = isDark
@@ -269,16 +428,31 @@ export function DataInspector({
269
428
  </ExtendedThemeContext.Provider>
270
429
  );
271
430
 
272
- // Wrap in StreamClickContext if a handler is provided
431
+ let wrapped = content;
432
+
273
433
  if (onStreamClick) {
274
- return (
434
+ wrapped = (
275
435
  <StreamClickContext.Provider value={onStreamClick}>
276
- {content}
436
+ {wrapped}
277
437
  </StreamClickContext.Provider>
278
438
  );
279
439
  }
440
+ if (onRunClick) {
441
+ wrapped = (
442
+ <RunClickContext.Provider value={onRunClick}>
443
+ {wrapped}
444
+ </RunClickContext.Provider>
445
+ );
446
+ }
447
+ if (onDecrypt) {
448
+ wrapped = (
449
+ <DecryptClickContext.Provider value={{ onDecrypt, isDecrypting }}>
450
+ {wrapped}
451
+ </DecryptClickContext.Provider>
452
+ );
453
+ }
280
454
 
281
- return content;
455
+ return wrapped;
282
456
  }
283
457
 
284
458
  function useStableInspectorData<T>(next: T): T {
@@ -767,6 +767,7 @@ export const WorkflowTraceViewer = ({
767
767
  onResolveHook,
768
768
  onCancelRun,
769
769
  onStreamClick,
770
+ onRunClick,
770
771
  onSpanSelect,
771
772
  onLoadEventData,
772
773
  onLoadMoreSpans,
@@ -775,6 +776,7 @@ export const WorkflowTraceViewer = ({
775
776
  encryptionKey,
776
777
  onDecrypt,
777
778
  isDecrypting = false,
779
+ hasEncryptedData = false,
778
780
  }: {
779
781
  run: WorkflowRun;
780
782
  events: Event[];
@@ -796,6 +798,8 @@ export const WorkflowTraceViewer = ({
796
798
  onCancelRun?: (runId: string) => Promise<void>;
797
799
  /** Callback when a stream reference is clicked in the detail panel */
798
800
  onStreamClick?: (streamId: string) => void;
801
+ /** Callback when a run reference is clicked in the detail panel */
802
+ onRunClick?: (runId: string) => void;
799
803
  /** Callback when a span is selected. */
800
804
  onSpanSelect?: (info: SpanSelectionInfo) => void;
801
805
  /** Callback to load event data for a specific event (lazy loading in sidebar) */
@@ -815,6 +819,8 @@ export const WorkflowTraceViewer = ({
815
819
  onDecrypt?: () => void;
816
820
  /** Whether the encryption key is currently being fetched */
817
821
  isDecrypting?: boolean;
822
+ /** Run-level hint: the run contains encrypted data (from probe). */
823
+ hasEncryptedData?: boolean;
818
824
  }) => {
819
825
  const toast = useToast();
820
826
  const [selectedSpan, setSelectedSpan] = useState<SelectedSpanInfo | null>(
@@ -903,6 +909,12 @@ export const WorkflowTraceViewer = ({
903
909
  });
904
910
  }, [events, selectedSpan?.spanId]);
905
911
 
912
+ // Reset selected span when navigating to a different run
913
+ useEffect(() => {
914
+ setSelectedSpan(null);
915
+ setDeselectTrigger((n) => n + 1);
916
+ }, [run?.runId]);
917
+
906
918
  const handleClose = useCallback(() => {
907
919
  setSelectedSpan(null);
908
920
  setDeselectTrigger((n) => n + 1);
@@ -1140,6 +1152,7 @@ export const WorkflowTraceViewer = ({
1140
1152
  <EntityDetailPanel
1141
1153
  run={run}
1142
1154
  onStreamClick={onStreamClick}
1155
+ onRunClick={onRunClick}
1143
1156
  spanDetailData={spanDetailData ?? null}
1144
1157
  spanDetailError={spanDetailError}
1145
1158
  spanDetailLoading={spanDetailLoading}
@@ -1151,6 +1164,7 @@ export const WorkflowTraceViewer = ({
1151
1164
  onDecrypt={onDecrypt}
1152
1165
  isDecrypting={isDecrypting}
1153
1166
  selectedSpan={selectedSpan}
1167
+ hasEncryptedData={hasEncryptedData}
1154
1168
  />
1155
1169
  </ErrorBoundary>
1156
1170
  </div>
@@ -225,7 +225,7 @@ export function stepToSpan(stepEvents: Event[], maxEndTime: Date): Span | null {
225
225
 
226
226
  return {
227
227
  spanId: String(step.stepId),
228
- name: parsedName?.shortName ?? '',
228
+ name: parsedName?.shortName ?? String(step.stepName),
229
229
  kind: 1, // INTERNAL span kind
230
230
  resource,
231
231
  library: WORKFLOW_LIBRARY,
package/src/index.ts CHANGED
@@ -40,6 +40,7 @@ export {
40
40
  ENCRYPTED_PLACEHOLDER,
41
41
  extractStreamIds,
42
42
  getWebRevivers,
43
+ hasEncryptedFields,
43
44
  hydrateResourceIO,
44
45
  hydrateResourceIOWithKey,
45
46
  isClassInstanceRef,
@@ -11,8 +11,10 @@ import {
11
11
  hydrateResourceIO as hydrateResourceIOGeneric,
12
12
  isEncryptedData,
13
13
  isExpiredStub,
14
+ isRunRef,
14
15
  observabilityRevivers,
15
16
  type Revivers,
17
+ serializedInstanceToRef,
16
18
  } from '@workflow/core/serialization-format';
17
19
  import { EVENT_DATA_REF_FIELDS } from '@workflow/world';
18
20
 
@@ -24,11 +26,15 @@ export {
24
26
  extractStreamIds,
25
27
  isClassInstanceRef,
26
28
  isEncryptedData,
29
+ isRunRef,
27
30
  isStreamId,
28
31
  isStreamRef,
29
32
  type Revivers,
33
+ RUN_REF_TYPE,
34
+ type RunRef,
30
35
  STREAM_REF_TYPE,
31
36
  type StreamRef,
37
+ serializedInstanceToRef,
32
38
  truncateId,
33
39
  } from '@workflow/core/serialization-format';
34
40
 
@@ -137,6 +143,11 @@ export function getWebRevivers(): Revivers {
137
143
  // react-inspector shows the class name (it reads constructor.name).
138
144
  Class: (value) => `<class:${extractClassName(value.classId)}>`,
139
145
  Instance: (value) => {
146
+ // Run instances are rendered as clickable RunRef badges
147
+ const runRef = serializedInstanceToRef(value);
148
+ if (isRunRef(runRef)) {
149
+ return runRef;
150
+ }
140
151
  const className = extractClassName(value.classId);
141
152
  const data = value.data;
142
153
  const props =
@@ -359,3 +370,29 @@ export async function hydrateResourceIOWithKey<T>(
359
370
 
360
371
  return result as T;
361
372
  }
373
+
374
+ /**
375
+ * Check whether a hydrated resource (event, step, run, etc.) contains any
376
+ * encrypted display markers. Inspects the standard top-level fields
377
+ * (`input`, `output`, `error`, `metadata`) as well as the event-type-specific
378
+ * `eventData` ref fields.
379
+ */
380
+ export function hasEncryptedFields(resource: unknown): boolean {
381
+ if (!resource || typeof resource !== 'object') return false;
382
+ const r = resource as Record<string, unknown>;
383
+
384
+ for (const key of ['input', 'output', 'metadata', 'error']) {
385
+ if (isEncryptedMarker(r[key])) return true;
386
+ }
387
+
388
+ if (r.eventData && typeof r.eventData === 'object') {
389
+ const eventType = typeof r.eventType === 'string' ? r.eventType : '';
390
+ const refKeys = EVENT_DATA_REF_FIELDS[eventType] ?? [];
391
+ const ed = r.eventData as Record<string, unknown>;
392
+ for (const key of refKeys) {
393
+ if (key in ed && isEncryptedMarker(ed[key])) return true;
394
+ }
395
+ }
396
+
397
+ return false;
398
+ }