@workflow/web-shared 5.0.0-beta.0 → 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 +76 -70
- 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.d.ts +1 -1
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +26 -18
- 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 +9 -2
- package/dist/lib/hydration.d.ts.map +1 -1
- package/dist/lib/hydration.js +43 -14
- 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 +4 -4
- package/src/components/event-list-view.tsx +207 -193
- 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/sidebar/events-list.tsx +42 -32
- 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 +50 -13
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import type
|
|
3
|
+
import { EVENT_DATA_REF_FIELDS, type Event } from '@workflow/world';
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
5
|
import { isExpiredMarker } from '../../lib/hydration';
|
|
6
6
|
import { ErrorCard } from '../ui/error-card';
|
|
@@ -55,46 +55,56 @@ function EventItem({
|
|
|
55
55
|
const [isLoading, setIsLoading] = useState(false);
|
|
56
56
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
57
57
|
const wasExpandedRef = useRef(false);
|
|
58
|
+
/** Mirrors whether we have a fetched payload; avoids stale `loadedData` in load closure. */
|
|
59
|
+
const loadedDataRef = useRef<unknown | null>(null);
|
|
58
60
|
|
|
59
61
|
// Check if the event already has eventData from the store
|
|
60
62
|
const existingData =
|
|
61
63
|
'eventData' in event && event.eventData != null ? event.eventData : null;
|
|
62
|
-
const
|
|
64
|
+
const mergedDisplay = loadedData ?? existingData;
|
|
63
65
|
const canHaveData = DATA_EVENT_TYPES.has(event.eventType);
|
|
64
66
|
|
|
65
|
-
const loadEventData = useCallback(
|
|
66
|
-
|
|
67
|
+
const loadEventData = useCallback(
|
|
68
|
+
async (options?: { force?: boolean }) => {
|
|
69
|
+
if (!onLoadEventData || !event.correlationId || !event.eventId) return;
|
|
70
|
+
if (!options?.force && loadedDataRef.current !== null) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
67
73
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
try {
|
|
75
|
+
setIsLoading(true);
|
|
76
|
+
setLoadError(null);
|
|
77
|
+
const data = await onLoadEventData(event.correlationId, event.eventId);
|
|
78
|
+
loadedDataRef.current = data;
|
|
79
|
+
setLoadedData(data);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
setLoadError(err instanceof Error ? err.message : String(err));
|
|
82
|
+
} finally {
|
|
83
|
+
setIsLoading(false);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
[onLoadEventData, event.correlationId, event.eventId]
|
|
87
|
+
);
|
|
79
88
|
|
|
80
89
|
const handleExpand = useCallback(async () => {
|
|
81
|
-
if (
|
|
90
|
+
if (isLoading) return;
|
|
82
91
|
wasExpandedRef.current = true;
|
|
83
92
|
await loadEventData();
|
|
84
|
-
}, [
|
|
93
|
+
}, [isLoading, loadEventData]);
|
|
85
94
|
|
|
86
95
|
// When the encryption key changes and this event was previously expanded,
|
|
87
96
|
// re-load the data so it gets decrypted
|
|
88
97
|
useEffect(() => {
|
|
89
|
-
if (encryptionKey
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
}, [encryptionKey]);
|
|
98
|
+
if (!encryptionKey || !wasExpandedRef.current) return;
|
|
99
|
+
loadedDataRef.current = null;
|
|
100
|
+
setLoadedData(null);
|
|
101
|
+
void loadEventData({ force: true });
|
|
102
|
+
}, [encryptionKey, loadEventData]);
|
|
95
103
|
|
|
96
104
|
const createdAt = new Date(event.createdAt);
|
|
97
105
|
|
|
106
|
+
const displayPayload = isLoading ? loadedData : mergedDisplay;
|
|
107
|
+
|
|
98
108
|
return (
|
|
99
109
|
<DetailCard
|
|
100
110
|
summaryClassName="text-base py-2"
|
|
@@ -189,9 +199,9 @@ function EventItem({
|
|
|
189
199
|
)}
|
|
190
200
|
|
|
191
201
|
{/* Event data */}
|
|
192
|
-
{
|
|
202
|
+
{displayPayload != null && (
|
|
193
203
|
<div className="mt-2">
|
|
194
|
-
<EventDataBlock eventType={event.eventType} data={
|
|
204
|
+
<EventDataBlock eventType={event.eventType} data={displayPayload} />
|
|
195
205
|
</div>
|
|
196
206
|
)}
|
|
197
207
|
</DetailCard>
|
|
@@ -199,17 +209,17 @@ function EventItem({
|
|
|
199
209
|
}
|
|
200
210
|
|
|
201
211
|
/**
|
|
202
|
-
* Check if an eventData object has only expired marker values in
|
|
203
|
-
*
|
|
204
|
-
*
|
|
212
|
+
* Check if an eventData object has only expired marker values in ref/payload
|
|
213
|
+
* fields for this event type (see {@link EVENT_DATA_REF_FIELDS}). Other keys
|
|
214
|
+
* (e.g. `resumeAt`, `stepName`) are ignored.
|
|
205
215
|
*/
|
|
206
|
-
function hasOnlyExpiredFields(data: unknown): boolean {
|
|
216
|
+
function hasOnlyExpiredFields(data: unknown, eventType: string): boolean {
|
|
207
217
|
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
208
218
|
return false;
|
|
209
219
|
}
|
|
210
220
|
const record = data as Record<string, unknown>;
|
|
211
|
-
const
|
|
212
|
-
const presentKeys =
|
|
221
|
+
const refKeys = EVENT_DATA_REF_FIELDS[eventType] ?? [];
|
|
222
|
+
const presentKeys = refKeys.filter((k) => k in record);
|
|
213
223
|
return (
|
|
214
224
|
presentKeys.length > 0 &&
|
|
215
225
|
presentKeys.every((k) => isExpiredMarker(record[k]))
|
|
@@ -230,7 +240,7 @@ function EventDataBlock({
|
|
|
230
240
|
// Expired data — show a simple message instead of the raw stub.
|
|
231
241
|
// Check both the top-level eventData and nested sub-fields (result, input, etc.)
|
|
232
242
|
// since the server stubs each ref field independently.
|
|
233
|
-
if (isExpiredMarker(data) || hasOnlyExpiredFields(data)) {
|
|
243
|
+
if (isExpiredMarker(data) || hasOnlyExpiredFields(data, eventType)) {
|
|
234
244
|
return (
|
|
235
245
|
<div
|
|
236
246
|
className="flex items-center gap-1.5 rounded-md border px-3 py-2 text-xs"
|
|
@@ -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,9 +11,12 @@ 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';
|
|
19
|
+
import { EVENT_DATA_REF_FIELDS } from '@workflow/world';
|
|
17
20
|
|
|
18
21
|
// Re-export types and utilities that consumers need
|
|
19
22
|
export {
|
|
@@ -23,11 +26,15 @@ export {
|
|
|
23
26
|
extractStreamIds,
|
|
24
27
|
isClassInstanceRef,
|
|
25
28
|
isEncryptedData,
|
|
29
|
+
isRunRef,
|
|
26
30
|
isStreamId,
|
|
27
31
|
isStreamRef,
|
|
28
32
|
type Revivers,
|
|
33
|
+
RUN_REF_TYPE,
|
|
34
|
+
type RunRef,
|
|
29
35
|
STREAM_REF_TYPE,
|
|
30
36
|
type StreamRef,
|
|
37
|
+
serializedInstanceToRef,
|
|
31
38
|
truncateId,
|
|
32
39
|
} from '@workflow/core/serialization-format';
|
|
33
40
|
|
|
@@ -136,6 +143,11 @@ export function getWebRevivers(): Revivers {
|
|
|
136
143
|
// react-inspector shows the class name (it reads constructor.name).
|
|
137
144
|
Class: (value) => `<class:${extractClassName(value.classId)}>`,
|
|
138
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
|
+
}
|
|
139
151
|
const className = extractClassName(value.classId);
|
|
140
152
|
const data = value.data;
|
|
141
153
|
const props =
|
|
@@ -274,9 +286,14 @@ function replaceEncryptedAndExpiredWithMarkers<T>(resource: T): T {
|
|
|
274
286
|
}
|
|
275
287
|
|
|
276
288
|
if (result.eventData && typeof result.eventData === 'object') {
|
|
289
|
+
const eventType =
|
|
290
|
+
typeof result.eventType === 'string' ? result.eventType : '';
|
|
291
|
+
const refKeys = EVENT_DATA_REF_FIELDS[eventType] ?? [];
|
|
277
292
|
const ed = { ...(result.eventData as Record<string, unknown>) };
|
|
278
|
-
for (const key of
|
|
279
|
-
|
|
293
|
+
for (const key of refKeys) {
|
|
294
|
+
if (key in ed) {
|
|
295
|
+
ed[key] = toDisplayMarker(ed[key]);
|
|
296
|
+
}
|
|
280
297
|
}
|
|
281
298
|
result.eventData = ed;
|
|
282
299
|
}
|
|
@@ -284,15 +301,6 @@ function replaceEncryptedAndExpiredWithMarkers<T>(resource: T): T {
|
|
|
284
301
|
return result as T;
|
|
285
302
|
}
|
|
286
303
|
|
|
287
|
-
/** Known serialized subfields within eventData, matching hydrateEventData in core */
|
|
288
|
-
const EVENT_DATA_SERIALIZED_FIELDS = [
|
|
289
|
-
'result',
|
|
290
|
-
'input',
|
|
291
|
-
'output',
|
|
292
|
-
'metadata',
|
|
293
|
-
'payload',
|
|
294
|
-
];
|
|
295
|
-
|
|
296
304
|
/**
|
|
297
305
|
* Hydrate resource data with decryption support.
|
|
298
306
|
*
|
|
@@ -300,7 +308,7 @@ const EVENT_DATA_SERIALIZED_FIELDS = [
|
|
|
300
308
|
* This is the async version used when the user clicks "Decrypt" in the web UI.
|
|
301
309
|
*
|
|
302
310
|
* Handles both top-level fields (input, output, metadata) and nested
|
|
303
|
-
* eventData subfields
|
|
311
|
+
* eventData subfields per `EVENT_DATA_REF_FIELDS` from `@workflow/world` for that event type.
|
|
304
312
|
*/
|
|
305
313
|
export async function hydrateResourceIOWithKey<T>(
|
|
306
314
|
resource: T,
|
|
@@ -344,8 +352,11 @@ export async function hydrateResourceIOWithKey<T>(
|
|
|
344
352
|
|
|
345
353
|
// Decrypt + hydrate eventData subfields (events)
|
|
346
354
|
if (result.eventData && typeof result.eventData === 'object') {
|
|
355
|
+
const eventType =
|
|
356
|
+
typeof result.eventType === 'string' ? result.eventType : '';
|
|
357
|
+
const refKeys = EVENT_DATA_REF_FIELDS[eventType] ?? [];
|
|
347
358
|
const eventData = { ...(result.eventData as Record<string, unknown>) };
|
|
348
|
-
for (const field of
|
|
359
|
+
for (const field of refKeys) {
|
|
349
360
|
if (field in eventData) {
|
|
350
361
|
eventData[field] = await decryptField(
|
|
351
362
|
eventData[field],
|
|
@@ -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
|
+
}
|