@workflow/web-shared 4.1.1 → 4.1.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/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/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/sidebar/attribute-panel.d.ts +5 -1
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +37 -25
- package/dist/components/sidebar/attribute-panel.js.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts +3 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +4 -3
- package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
- package/dist/components/ui/data-inspector.d.ts +14 -1
- package/dist/components/ui/data-inspector.d.ts.map +1 -1
- package/dist/components/ui/data-inspector.js +36 -11
- package/dist/components/ui/data-inspector.js.map +1 -1
- package/dist/components/workflow-trace-view.d.ts +3 -1
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +2 -2
- package/dist/components/workflow-trace-view.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/hydration.d.ts +7 -0
- package/dist/lib/hydration.d.ts.map +1 -1
- package/dist/lib/hydration.js +25 -0
- package/dist/lib/hydration.js.map +1 -1
- package/package.json +2 -2
- package/src/components/event-list-view.tsx +181 -173
- package/src/components/index.ts +6 -1
- package/src/components/sidebar/attribute-panel.tsx +150 -106
- package/src/components/sidebar/entity-detail-panel.tsx +13 -7
- package/src/components/ui/data-inspector.tsx +91 -19
- package/src/components/workflow-trace-view.tsx +4 -0
- package/src/index.ts +1 -0
- package/src/lib/hydration.ts +26 -0
|
@@ -5,11 +5,11 @@ import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
|
|
|
5
5
|
import type { ModelMessage } from 'ai';
|
|
6
6
|
import { Lock } from 'lucide-react';
|
|
7
7
|
import type { KeyboardEvent, ReactNode } from 'react';
|
|
8
|
-
import { useCallback, useMemo, useState } from 'react';
|
|
8
|
+
import { useCallback, useContext, useMemo, useState } from 'react';
|
|
9
9
|
import { isEncryptedMarker, isExpiredMarker } from '../../lib/hydration';
|
|
10
10
|
import { useToast } from '../../lib/toast';
|
|
11
11
|
import { extractConversation, isDoStreamStep } from '../../lib/utils';
|
|
12
|
-
import { StreamClickContext } from '../ui/data-inspector';
|
|
12
|
+
import { DecryptClickContext, StreamClickContext } from '../ui/data-inspector';
|
|
13
13
|
import { ErrorCard } from '../ui/error-card';
|
|
14
14
|
import {
|
|
15
15
|
ErrorStackBlock,
|
|
@@ -180,9 +180,42 @@ function ConversationWithTabs({
|
|
|
180
180
|
* with the lucide Lock icon matching the title bar Decrypt button.
|
|
181
181
|
*/
|
|
182
182
|
function EncryptedFieldBlock() {
|
|
183
|
+
const ctx = useContext(DecryptClickContext);
|
|
184
|
+
if (ctx) {
|
|
185
|
+
return (
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
188
|
+
onClick={ctx.onDecrypt}
|
|
189
|
+
disabled={ctx.isDecrypting}
|
|
190
|
+
className="flex w-full items-center justify-center gap-1.5 rounded-md border px-3 py-2 text-xs cursor-pointer transition-colors"
|
|
191
|
+
style={{
|
|
192
|
+
borderColor: 'var(--ds-gray-400)',
|
|
193
|
+
backgroundColor: 'var(--ds-gray-100)',
|
|
194
|
+
color: 'var(--ds-gray-700)',
|
|
195
|
+
opacity: ctx.isDecrypting ? 0.6 : 1,
|
|
196
|
+
}}
|
|
197
|
+
title="Click to decrypt"
|
|
198
|
+
>
|
|
199
|
+
{ctx.isDecrypting ? (
|
|
200
|
+
<span
|
|
201
|
+
className="h-3 w-3 animate-spin rounded-full border-2"
|
|
202
|
+
style={{
|
|
203
|
+
borderColor: 'var(--ds-gray-400)',
|
|
204
|
+
borderTopColor: 'var(--ds-gray-700)',
|
|
205
|
+
}}
|
|
206
|
+
/>
|
|
207
|
+
) : (
|
|
208
|
+
<Lock className="h-3 w-3" />
|
|
209
|
+
)}
|
|
210
|
+
<span className="font-medium">
|
|
211
|
+
{ctx.isDecrypting ? 'Decrypting…' : 'Decrypt'}
|
|
212
|
+
</span>
|
|
213
|
+
</button>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
183
216
|
return (
|
|
184
217
|
<div
|
|
185
|
-
className="flex items-center gap-1.5 rounded-md border px-3 py-2 text-xs"
|
|
218
|
+
className="flex w-full items-center justify-center gap-1.5 rounded-md border px-3 py-2 text-xs"
|
|
186
219
|
style={{
|
|
187
220
|
borderColor: 'var(--ds-gray-300)',
|
|
188
221
|
backgroundColor: 'var(--ds-gray-100)',
|
|
@@ -689,6 +722,8 @@ export const AttributePanel = ({
|
|
|
689
722
|
error,
|
|
690
723
|
expiredAt,
|
|
691
724
|
onStreamClick,
|
|
725
|
+
onDecrypt,
|
|
726
|
+
isDecrypting = false,
|
|
692
727
|
resource,
|
|
693
728
|
}: {
|
|
694
729
|
data: Record<string, unknown>;
|
|
@@ -698,6 +733,10 @@ export const AttributePanel = ({
|
|
|
698
733
|
expiredAt?: string | Date;
|
|
699
734
|
/** Callback when a stream reference is clicked */
|
|
700
735
|
onStreamClick?: (streamId: string) => void;
|
|
736
|
+
/** Callback when an encrypted marker is clicked (triggers decryption) */
|
|
737
|
+
onDecrypt?: () => void;
|
|
738
|
+
/** Whether decryption is currently in progress */
|
|
739
|
+
isDecrypting?: boolean;
|
|
701
740
|
/** Resource type of the selected span — used to show targeted loading skeletons. */
|
|
702
741
|
resource?: string;
|
|
703
742
|
}) => {
|
|
@@ -798,116 +837,121 @@ export const AttributePanel = ({
|
|
|
798
837
|
|
|
799
838
|
return (
|
|
800
839
|
<StreamClickContext.Provider value={onStreamClick}>
|
|
801
|
-
<
|
|
802
|
-
{
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
showResumeAtSkeleton
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
840
|
+
<DecryptClickContext.Provider
|
|
841
|
+
value={onDecrypt ? { onDecrypt, isDecrypting } : undefined}
|
|
842
|
+
>
|
|
843
|
+
<div>
|
|
844
|
+
{/* Basic attributes in a vertical layout with border */}
|
|
845
|
+
{visibleBasicAttributes.length > 0 && (
|
|
846
|
+
<div
|
|
847
|
+
className="mb-3 flex flex-col overflow-hidden rounded-lg border"
|
|
848
|
+
style={{
|
|
849
|
+
borderColor: 'var(--ds-gray-300)',
|
|
850
|
+
}}
|
|
851
|
+
>
|
|
852
|
+
{orderedBasicAttributes.map((attribute, index) => {
|
|
853
|
+
const displayValue = attributeToDisplayFn[
|
|
854
|
+
attribute as keyof typeof attributeToDisplayFn
|
|
855
|
+
]?.(displayData[attribute as keyof typeof displayData]);
|
|
856
|
+
const isModuleSpecifier = attribute === 'moduleSpecifier';
|
|
857
|
+
const moduleSpecifierValue =
|
|
858
|
+
typeof displayValue === 'string'
|
|
859
|
+
? displayValue
|
|
860
|
+
: String(displayValue ?? displayData.moduleSpecifier ?? '');
|
|
861
|
+
const shouldCapitalizeLabel =
|
|
862
|
+
attribute !== 'workflowCoreVersion';
|
|
863
|
+
const showResumeAtSkeleton =
|
|
864
|
+
isLoading && resource === 'sleep' && !displayData.resumeAt;
|
|
865
|
+
const showDivider =
|
|
866
|
+
index < orderedBasicAttributes.length - 1 ||
|
|
867
|
+
showResumeAtSkeleton;
|
|
868
|
+
|
|
869
|
+
return (
|
|
870
|
+
<div key={attribute} className="py-1">
|
|
871
|
+
<div className="flex min-h-[32px] items-center justify-between gap-4 rounded-sm px-2.5 py-1">
|
|
872
|
+
<span
|
|
873
|
+
className={
|
|
874
|
+
shouldCapitalizeLabel
|
|
875
|
+
? 'text-[14px] first-letter:uppercase'
|
|
876
|
+
: 'text-[14px]'
|
|
877
|
+
}
|
|
878
|
+
style={{ color: 'var(--ds-gray-700)' }}
|
|
879
|
+
>
|
|
880
|
+
{getAttributeDisplayName(attribute)}
|
|
881
|
+
</span>
|
|
882
|
+
{isModuleSpecifier ? (
|
|
883
|
+
<button
|
|
884
|
+
type="button"
|
|
885
|
+
className="min-w-0 max-w-[70%] truncate text-right text-[13px] font-mono"
|
|
886
|
+
style={{
|
|
887
|
+
color: 'var(--ds-gray-1000)',
|
|
888
|
+
background: 'transparent',
|
|
889
|
+
border: 'none',
|
|
890
|
+
padding: 0,
|
|
891
|
+
}}
|
|
892
|
+
title={moduleSpecifierValue}
|
|
893
|
+
onClick={() =>
|
|
894
|
+
handleCopyModuleSpecifier(moduleSpecifierValue)
|
|
895
|
+
}
|
|
896
|
+
>
|
|
897
|
+
{moduleSpecifierValue}
|
|
898
|
+
</button>
|
|
899
|
+
) : (
|
|
900
|
+
<span
|
|
901
|
+
className="min-w-0 max-w-[70%] truncate text-right text-[13px] font-mono"
|
|
902
|
+
style={{ color: 'var(--ds-gray-1000)' }}
|
|
903
|
+
>
|
|
904
|
+
{displayValue}
|
|
905
|
+
</span>
|
|
906
|
+
)}
|
|
907
|
+
</div>
|
|
908
|
+
{showDivider ? (
|
|
909
|
+
<div
|
|
910
|
+
className="mx-2.5 border-b"
|
|
911
|
+
style={{ borderColor: 'var(--ds-gray-300)' }}
|
|
912
|
+
/>
|
|
913
|
+
) : null}
|
|
914
|
+
</div>
|
|
915
|
+
);
|
|
916
|
+
})}
|
|
917
|
+
{isLoading && resource === 'sleep' && !displayData.resumeAt && (
|
|
918
|
+
<div className="py-1">
|
|
828
919
|
<div className="flex min-h-[32px] items-center justify-between gap-4 rounded-sm px-2.5 py-1">
|
|
829
920
|
<span
|
|
830
|
-
className=
|
|
831
|
-
shouldCapitalizeLabel
|
|
832
|
-
? 'text-[14px] first-letter:uppercase'
|
|
833
|
-
: 'text-[14px]'
|
|
834
|
-
}
|
|
921
|
+
className="text-[14px] first-letter:uppercase"
|
|
835
922
|
style={{ color: 'var(--ds-gray-700)' }}
|
|
836
923
|
>
|
|
837
|
-
|
|
924
|
+
resumeAt
|
|
838
925
|
</span>
|
|
839
|
-
|
|
840
|
-
<button
|
|
841
|
-
type="button"
|
|
842
|
-
className="min-w-0 max-w-[70%] truncate text-right text-[13px] font-mono"
|
|
843
|
-
style={{
|
|
844
|
-
color: 'var(--ds-gray-1000)',
|
|
845
|
-
background: 'transparent',
|
|
846
|
-
border: 'none',
|
|
847
|
-
padding: 0,
|
|
848
|
-
}}
|
|
849
|
-
title={moduleSpecifierValue}
|
|
850
|
-
onClick={() =>
|
|
851
|
-
handleCopyModuleSpecifier(moduleSpecifierValue)
|
|
852
|
-
}
|
|
853
|
-
>
|
|
854
|
-
{moduleSpecifierValue}
|
|
855
|
-
</button>
|
|
856
|
-
) : (
|
|
857
|
-
<span
|
|
858
|
-
className="min-w-0 max-w-[70%] truncate text-right text-[13px] font-mono"
|
|
859
|
-
style={{ color: 'var(--ds-gray-1000)' }}
|
|
860
|
-
>
|
|
861
|
-
{displayValue}
|
|
862
|
-
</span>
|
|
863
|
-
)}
|
|
926
|
+
<Skeleton className="h-4 w-[55%]" />
|
|
864
927
|
</div>
|
|
865
|
-
{showDivider ? (
|
|
866
|
-
<div
|
|
867
|
-
className="mx-2.5 border-b"
|
|
868
|
-
style={{ borderColor: 'var(--ds-gray-300)' }}
|
|
869
|
-
/>
|
|
870
|
-
) : null}
|
|
871
|
-
</div>
|
|
872
|
-
);
|
|
873
|
-
})}
|
|
874
|
-
{isLoading && resource === 'sleep' && !displayData.resumeAt && (
|
|
875
|
-
<div className="py-1">
|
|
876
|
-
<div className="flex min-h-[32px] items-center justify-between gap-4 rounded-sm px-2.5 py-1">
|
|
877
|
-
<span
|
|
878
|
-
className="text-[14px] first-letter:uppercase"
|
|
879
|
-
style={{ color: 'var(--ds-gray-700)' }}
|
|
880
|
-
>
|
|
881
|
-
resumeAt
|
|
882
|
-
</span>
|
|
883
|
-
<Skeleton className="h-4 w-[55%]" />
|
|
884
928
|
</div>
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
</
|
|
929
|
+
)}
|
|
930
|
+
</div>
|
|
931
|
+
)}
|
|
932
|
+
{error ? (
|
|
933
|
+
<ErrorCard
|
|
934
|
+
title="Failed to load resource details"
|
|
935
|
+
details={error.message}
|
|
936
|
+
className="my-4"
|
|
937
|
+
/>
|
|
938
|
+
) : hasExpired ? (
|
|
939
|
+
<ExpiredDataMessage />
|
|
940
|
+
) : (
|
|
941
|
+
<>
|
|
942
|
+
{resolvedAttributes.map((attribute) => (
|
|
943
|
+
<AttributeBlock
|
|
944
|
+
isLoading={isLoading}
|
|
945
|
+
key={attribute}
|
|
946
|
+
attribute={attribute}
|
|
947
|
+
value={displayData[attribute as keyof typeof displayData]}
|
|
948
|
+
context={displayContext}
|
|
949
|
+
/>
|
|
950
|
+
))}
|
|
951
|
+
</>
|
|
952
|
+
)}
|
|
953
|
+
</div>
|
|
954
|
+
</DecryptClickContext.Provider>
|
|
911
955
|
</StreamClickContext.Provider>
|
|
912
956
|
);
|
|
913
957
|
};
|
|
@@ -67,6 +67,7 @@ export function EntityDetailPanel({
|
|
|
67
67
|
onDecrypt,
|
|
68
68
|
isDecrypting = false,
|
|
69
69
|
selectedSpan,
|
|
70
|
+
hasEncryptedData = false,
|
|
70
71
|
}: {
|
|
71
72
|
run: WorkflowRun;
|
|
72
73
|
/** Callback when a stream reference is clicked */
|
|
@@ -103,6 +104,8 @@ export function EntityDetailPanel({
|
|
|
103
104
|
isDecrypting?: boolean;
|
|
104
105
|
/** Info about the currently selected span from the trace viewer */
|
|
105
106
|
selectedSpan: SelectedSpanInfo | null;
|
|
107
|
+
/** Run-level hint: the run contains encrypted data (from probe). */
|
|
108
|
+
hasEncryptedData?: boolean;
|
|
106
109
|
}): React.JSX.Element | null {
|
|
107
110
|
const toast = useToast();
|
|
108
111
|
const [stoppingSleep, setStoppingSleep] = useState(false);
|
|
@@ -391,13 +394,14 @@ export function EntityDetailPanel({
|
|
|
391
394
|
{resourceId}
|
|
392
395
|
</p>
|
|
393
396
|
</div>
|
|
394
|
-
{(hasEncryptedFields || encryptionKey) &&
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
397
|
+
{(hasEncryptedFields || hasEncryptedData || encryptionKey) &&
|
|
398
|
+
onDecrypt && (
|
|
399
|
+
<DecryptButton
|
|
400
|
+
decrypted={!!encryptionKey}
|
|
401
|
+
loading={isDecrypting}
|
|
402
|
+
onClick={onDecrypt}
|
|
403
|
+
/>
|
|
404
|
+
)}
|
|
401
405
|
</div>
|
|
402
406
|
</div>
|
|
403
407
|
|
|
@@ -481,6 +485,8 @@ export function EntityDetailPanel({
|
|
|
481
485
|
isLoading={loading}
|
|
482
486
|
error={error ?? undefined}
|
|
483
487
|
onStreamClick={onStreamClick}
|
|
488
|
+
onDecrypt={onDecrypt}
|
|
489
|
+
isDecrypting={isDecrypting}
|
|
484
490
|
resource={resource}
|
|
485
491
|
/>
|
|
486
492
|
</section>
|
|
@@ -75,6 +75,76 @@ export const StreamClickContext = createContext<
|
|
|
75
75
|
((streamId: string) => void) | undefined
|
|
76
76
|
>(undefined);
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Context for passing a decrypt handler down to DataInspector instances.
|
|
80
|
+
* When provided, encrypted markers become clickable buttons that trigger decryption.
|
|
81
|
+
*/
|
|
82
|
+
export type DecryptClickContextValue = {
|
|
83
|
+
onDecrypt: () => void;
|
|
84
|
+
isDecrypting: boolean;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const DecryptClickContext = createContext<
|
|
88
|
+
DecryptClickContextValue | undefined
|
|
89
|
+
>(undefined);
|
|
90
|
+
|
|
91
|
+
function EncryptedInlineLabel() {
|
|
92
|
+
const ctx = useContext(DecryptClickContext);
|
|
93
|
+
if (ctx) {
|
|
94
|
+
return (
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[11px] cursor-pointer"
|
|
98
|
+
style={{
|
|
99
|
+
backgroundColor: 'var(--ds-gray-100)',
|
|
100
|
+
color: 'var(--ds-gray-700)',
|
|
101
|
+
border: '1px solid var(--ds-gray-400)',
|
|
102
|
+
fontStyle: 'italic',
|
|
103
|
+
opacity: ctx.isDecrypting ? 0.6 : 1,
|
|
104
|
+
}}
|
|
105
|
+
disabled={ctx.isDecrypting}
|
|
106
|
+
onClick={(e) => {
|
|
107
|
+
e.stopPropagation();
|
|
108
|
+
ctx.onDecrypt();
|
|
109
|
+
}}
|
|
110
|
+
title="Click to decrypt"
|
|
111
|
+
>
|
|
112
|
+
{ctx.isDecrypting ? (
|
|
113
|
+
<span
|
|
114
|
+
className="h-3 w-3 animate-spin rounded-full border-2"
|
|
115
|
+
style={{
|
|
116
|
+
display: 'inline-block',
|
|
117
|
+
flexShrink: 0,
|
|
118
|
+
borderColor: 'var(--ds-gray-400)',
|
|
119
|
+
borderTopColor: 'var(--ds-gray-700)',
|
|
120
|
+
}}
|
|
121
|
+
/>
|
|
122
|
+
) : (
|
|
123
|
+
<Lock
|
|
124
|
+
className="h-3 w-3"
|
|
125
|
+
style={{ display: 'inline', flexShrink: 0 }}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
128
|
+
<span>{ctx.isDecrypting ? 'Decrypting…' : 'Decrypt'}</span>
|
|
129
|
+
</button>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return (
|
|
133
|
+
<span style={{ color: 'var(--ds-gray-600)', fontStyle: 'italic' }}>
|
|
134
|
+
<Lock
|
|
135
|
+
className="h-3 w-3"
|
|
136
|
+
style={{
|
|
137
|
+
display: 'inline',
|
|
138
|
+
verticalAlign: 'middle',
|
|
139
|
+
marginRight: '3px',
|
|
140
|
+
marginTop: '-1px',
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
Encrypted
|
|
144
|
+
</span>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
78
148
|
function StreamRefInline({ streamRef }: { streamRef: StreamRef }) {
|
|
79
149
|
const onStreamClick = useContext(StreamClickContext);
|
|
80
150
|
return (
|
|
@@ -132,26 +202,13 @@ function NodeRenderer({
|
|
|
132
202
|
}) {
|
|
133
203
|
const extendedTheme = useContext(ExtendedThemeContext);
|
|
134
204
|
|
|
135
|
-
// Encrypted marker → flat label with Lock icon,
|
|
205
|
+
// Encrypted marker → flat label with Lock icon, clickable when onDecrypt is available
|
|
136
206
|
if (
|
|
137
207
|
data !== null &&
|
|
138
208
|
typeof data === 'object' &&
|
|
139
209
|
data.constructor?.name === ENCRYPTED_DISPLAY_NAME
|
|
140
210
|
) {
|
|
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
|
-
);
|
|
211
|
+
const label = <EncryptedInlineLabel />;
|
|
155
212
|
if (depth === 0) {
|
|
156
213
|
return label;
|
|
157
214
|
}
|
|
@@ -234,6 +291,10 @@ export interface DataInspectorProps {
|
|
|
234
291
|
name?: string;
|
|
235
292
|
/** Callback when a stream reference is clicked */
|
|
236
293
|
onStreamClick?: (streamId: string) => void;
|
|
294
|
+
/** Callback when an encrypted marker is clicked (triggers decryption) */
|
|
295
|
+
onDecrypt?: () => void;
|
|
296
|
+
/** Whether decryption is currently in progress */
|
|
297
|
+
isDecrypting?: boolean;
|
|
237
298
|
}
|
|
238
299
|
|
|
239
300
|
export function DataInspector({
|
|
@@ -241,6 +302,8 @@ export function DataInspector({
|
|
|
241
302
|
expandLevel = 2,
|
|
242
303
|
name,
|
|
243
304
|
onStreamClick,
|
|
305
|
+
onDecrypt,
|
|
306
|
+
isDecrypting = false,
|
|
244
307
|
}: DataInspectorProps) {
|
|
245
308
|
const stableData = useStableInspectorData(data);
|
|
246
309
|
const [initialExpandLevel, setInitialExpandLevel] = useState(expandLevel);
|
|
@@ -269,16 +332,25 @@ export function DataInspector({
|
|
|
269
332
|
</ExtendedThemeContext.Provider>
|
|
270
333
|
);
|
|
271
334
|
|
|
272
|
-
|
|
335
|
+
let wrapped = content;
|
|
336
|
+
|
|
273
337
|
if (onStreamClick) {
|
|
274
|
-
|
|
338
|
+
wrapped = (
|
|
275
339
|
<StreamClickContext.Provider value={onStreamClick}>
|
|
276
|
-
{
|
|
340
|
+
{wrapped}
|
|
277
341
|
</StreamClickContext.Provider>
|
|
278
342
|
);
|
|
279
343
|
}
|
|
280
344
|
|
|
281
|
-
|
|
345
|
+
if (onDecrypt) {
|
|
346
|
+
wrapped = (
|
|
347
|
+
<DecryptClickContext.Provider value={{ onDecrypt, isDecrypting }}>
|
|
348
|
+
{wrapped}
|
|
349
|
+
</DecryptClickContext.Provider>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return wrapped;
|
|
282
354
|
}
|
|
283
355
|
|
|
284
356
|
function useStableInspectorData<T>(next: T): T {
|
|
@@ -775,6 +775,7 @@ export const WorkflowTraceViewer = ({
|
|
|
775
775
|
encryptionKey,
|
|
776
776
|
onDecrypt,
|
|
777
777
|
isDecrypting = false,
|
|
778
|
+
hasEncryptedData = false,
|
|
778
779
|
}: {
|
|
779
780
|
run: WorkflowRun;
|
|
780
781
|
events: Event[];
|
|
@@ -815,6 +816,8 @@ export const WorkflowTraceViewer = ({
|
|
|
815
816
|
onDecrypt?: () => void;
|
|
816
817
|
/** Whether the encryption key is currently being fetched */
|
|
817
818
|
isDecrypting?: boolean;
|
|
819
|
+
/** Run-level hint: the run contains encrypted data (from probe). */
|
|
820
|
+
hasEncryptedData?: boolean;
|
|
818
821
|
}) => {
|
|
819
822
|
const toast = useToast();
|
|
820
823
|
const [selectedSpan, setSelectedSpan] = useState<SelectedSpanInfo | null>(
|
|
@@ -1151,6 +1154,7 @@ export const WorkflowTraceViewer = ({
|
|
|
1151
1154
|
onDecrypt={onDecrypt}
|
|
1152
1155
|
isDecrypting={isDecrypting}
|
|
1153
1156
|
selectedSpan={selectedSpan}
|
|
1157
|
+
hasEncryptedData={hasEncryptedData}
|
|
1154
1158
|
/>
|
|
1155
1159
|
</ErrorBoundary>
|
|
1156
1160
|
</div>
|
package/src/index.ts
CHANGED
package/src/lib/hydration.ts
CHANGED
|
@@ -359,3 +359,29 @@ export async function hydrateResourceIOWithKey<T>(
|
|
|
359
359
|
|
|
360
360
|
return result as T;
|
|
361
361
|
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Check whether a hydrated resource (event, step, run, etc.) contains any
|
|
365
|
+
* encrypted display markers. Inspects the standard top-level fields
|
|
366
|
+
* (`input`, `output`, `error`, `metadata`) as well as the event-type-specific
|
|
367
|
+
* `eventData` ref fields.
|
|
368
|
+
*/
|
|
369
|
+
export function hasEncryptedFields(resource: unknown): boolean {
|
|
370
|
+
if (!resource || typeof resource !== 'object') return false;
|
|
371
|
+
const r = resource as Record<string, unknown>;
|
|
372
|
+
|
|
373
|
+
for (const key of ['input', 'output', 'metadata', 'error']) {
|
|
374
|
+
if (isEncryptedMarker(r[key])) return true;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (r.eventData && typeof r.eventData === 'object') {
|
|
378
|
+
const eventType = typeof r.eventType === 'string' ? r.eventType : '';
|
|
379
|
+
const refKeys = EVENT_DATA_REF_FIELDS[eventType] ?? [];
|
|
380
|
+
const ed = r.eventData as Record<string, unknown>;
|
|
381
|
+
for (const key of refKeys) {
|
|
382
|
+
if (key in ed && isEncryptedMarker(ed[key])) return true;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return false;
|
|
387
|
+
}
|