@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.
- package/README.md +1 -1
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/event-list-view.d.ts +3 -1
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +53 -53
- package/dist/components/event-list-view.js.map +1 -1
- package/dist/components/hook-actions.js.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/index.js.map +1 -1
- package/dist/components/run-trace-view.d.ts +2 -1
- package/dist/components/run-trace-view.d.ts.map +1 -1
- package/dist/components/run-trace-view.js +2 -2
- package/dist/components/run-trace-view.js.map +1 -1
- package/dist/components/sidebar/attribute-panel.d.ts +7 -1
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +36 -26
- package/dist/components/sidebar/attribute-panel.js.map +1 -1
- package/dist/components/sidebar/conversation-view.js.map +1 -1
- package/dist/components/sidebar/copyable-data-block.js.map +1 -1
- package/dist/components/sidebar/detail-card.js.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts +5 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +18 -16
- package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
- package/dist/components/sidebar/events-list.js.map +1 -1
- package/dist/components/sidebar/resolve-hook-modal.js.map +1 -1
- package/dist/components/stream-viewer.js.map +1 -1
- package/dist/components/trace-viewer/components/map.js.map +1 -1
- package/dist/components/trace-viewer/components/markers.js.map +1 -1
- package/dist/components/trace-viewer/components/node.js.map +1 -1
- package/dist/components/trace-viewer/components/search-input.js.map +1 -1
- package/dist/components/trace-viewer/components/search.js.map +1 -1
- package/dist/components/trace-viewer/components/span-content.js.map +1 -1
- package/dist/components/trace-viewer/components/span-detail-panel.js.map +1 -1
- package/dist/components/trace-viewer/components/span-segments.js.map +1 -1
- package/dist/components/trace-viewer/components/span-strategies.js.map +1 -1
- package/dist/components/trace-viewer/components/ui.js.map +1 -1
- package/dist/components/trace-viewer/components/zoom-button.js.map +1 -1
- package/dist/components/trace-viewer/components/zoom-icons.js.map +1 -1
- package/dist/components/trace-viewer/context.js.map +1 -1
- package/dist/components/trace-viewer/index.js.map +1 -1
- package/dist/components/trace-viewer/trace-viewer.js.map +1 -1
- package/dist/components/trace-viewer/types.js.map +1 -1
- package/dist/components/trace-viewer/util/constants.js.map +1 -1
- package/dist/components/trace-viewer/util/scrollbar-width.js.map +1 -1
- package/dist/components/trace-viewer/util/timing.js.map +1 -1
- package/dist/components/trace-viewer/util/tree.js.map +1 -1
- package/dist/components/trace-viewer/util/use-immediate-style.js.map +1 -1
- package/dist/components/trace-viewer/util/use-streaming-spans.js.map +1 -1
- package/dist/components/trace-viewer/util/use-trackpad-zoom.js.map +1 -1
- package/dist/components/trace-viewer/worker.js.map +1 -1
- package/dist/components/ui/alert.js.map +1 -1
- package/dist/components/ui/card.js.map +1 -1
- package/dist/components/ui/data-inspector.d.ts +17 -1
- package/dist/components/ui/data-inspector.d.ts.map +1 -1
- package/dist/components/ui/data-inspector.js +116 -21
- package/dist/components/ui/data-inspector.js.map +1 -1
- package/dist/components/ui/decrypt-button.js.map +1 -1
- package/dist/components/ui/error-card.js.map +1 -1
- package/dist/components/ui/error-stack-block.js.map +1 -1
- package/dist/components/ui/inspector-theme.js.map +1 -1
- package/dist/components/ui/load-more-button.js.map +1 -1
- package/dist/components/ui/menu-dropdown.js.map +1 -1
- package/dist/components/ui/skeleton.js.map +1 -1
- package/dist/components/ui/spinner.js.map +1 -1
- package/dist/components/ui/timestamp-tooltip.js.map +1 -1
- package/dist/components/workflow-trace-view.d.ts +5 -1
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +7 -2
- package/dist/components/workflow-trace-view.js.map +1 -1
- package/dist/components/workflow-traces/event-colors.js.map +1 -1
- package/dist/components/workflow-traces/trace-colors.js.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
- package/dist/components/workflow-traces/trace-time-utils.js.map +1 -1
- package/dist/hooks/use-dark-mode.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/event-analysis.js.map +1 -1
- package/dist/lib/event-materialization.js.map +1 -1
- package/dist/lib/hydration.d.ts +8 -1
- package/dist/lib/hydration.d.ts.map +1 -1
- package/dist/lib/hydration.js +32 -2
- package/dist/lib/hydration.js.map +1 -1
- package/dist/lib/toast.js.map +1 -1
- package/dist/lib/trace-builder.js.map +1 -1
- package/dist/lib/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/components/event-list-view.tsx +181 -173
- package/src/components/index.ts +6 -1
- package/src/components/run-trace-view.tsx +3 -0
- package/src/components/sidebar/attribute-panel.tsx +160 -109
- package/src/components/sidebar/entity-detail-panel.tsx +111 -96
- package/src/components/ui/data-inspector.tsx +205 -31
- package/src/components/workflow-trace-view.tsx +14 -0
- package/src/components/workflow-traces/trace-span-construction.ts +1 -1
- package/src/index.ts +1 -0
- package/src/lib/hydration.ts +37 -0
|
@@ -9,7 +9,14 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { Lock } from 'lucide-react';
|
|
12
|
-
import {
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
431
|
+
let wrapped = content;
|
|
432
|
+
|
|
273
433
|
if (onStreamClick) {
|
|
274
|
-
|
|
434
|
+
wrapped = (
|
|
275
435
|
<StreamClickContext.Provider value={onStreamClick}>
|
|
276
|
-
{
|
|
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
|
|
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
package/src/lib/hydration.ts
CHANGED
|
@@ -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
|
+
}
|