@workflow/web-shared 5.0.0-beta.1 → 5.0.0-beta.10
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 +1 -1
- package/dist/components/event-list-view.d.ts +18 -1
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +219 -125
- package/dist/components/hook-actions.js +1 -1
- package/dist/components/index.d.ts +4 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +5 -2
- package/dist/components/new-trace-viewer/components/copy-button.d.ts +9 -0
- package/dist/components/new-trace-viewer/components/copy-button.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/copy-button.js +27 -0
- package/dist/components/new-trace-viewer/components/event-list.d.ts +10 -0
- package/dist/components/new-trace-viewer/components/event-list.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/event-list.js +39 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts +18 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/copy-selection.js +54 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts +20 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/middle-truncate.js +81 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts +25 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js +101 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts +20 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.js +114 -0
- package/dist/components/new-trace-viewer/components/split-pane.d.ts +14 -0
- package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/split-pane.js +99 -0
- package/dist/components/new-trace-viewer/components/timeline.d.ts +34 -0
- package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/timeline.js +223 -0
- package/dist/components/new-trace-viewer/context.d.ts +15 -0
- package/dist/components/new-trace-viewer/context.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/context.js +47 -0
- package/dist/components/new-trace-viewer/detail-panel.d.ts +10 -0
- package/dist/components/new-trace-viewer/detail-panel.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/detail-panel.js +13 -0
- package/dist/components/new-trace-viewer/icons.d.ts +6 -0
- package/dist/components/new-trace-viewer/icons.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/icons.js +15 -0
- package/dist/components/new-trace-viewer/search.d.ts +19 -0
- package/dist/components/new-trace-viewer/search.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/search.js +134 -0
- package/dist/components/new-trace-viewer/trace-viewer.d.ts +8 -0
- package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/trace-viewer.js +387 -0
- package/dist/components/new-trace-viewer/types.d.ts +1 -0
- package/dist/components/new-trace-viewer/types.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/types.js +2 -0
- package/dist/components/new-trace-viewer/util/constants.d.ts +1 -0
- package/dist/components/new-trace-viewer/util/constants.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/util/constants.js +2 -0
- package/dist/components/new-trace-viewer/utils.d.ts +46 -0
- package/dist/components/new-trace-viewer/utils.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/utils.js +330 -0
- 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 +3 -3
- 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 +131 -70
- package/dist/components/sidebar/conversation-view.js +1 -1
- package/dist/components/sidebar/copyable-data-block.d.ts +1 -0
- package/dist/components/sidebar/copyable-data-block.d.ts.map +1 -1
- package/dist/components/sidebar/copyable-data-block.js +16 -15
- package/dist/components/sidebar/detail-card.d.ts +5 -6
- package/dist/components/sidebar/detail-card.d.ts.map +1 -1
- package/dist/components/sidebar/detail-card.js +32 -16
- 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 +20 -37
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +21 -15
- package/dist/components/sidebar/resolve-hook-modal.js +1 -1
- package/dist/components/sidebar/sidebar-data-context.d.ts +29 -0
- package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -0
- package/dist/components/sidebar/sidebar-data-context.js +19 -0
- package/dist/components/stream-viewer.d.ts +1 -1
- package/dist/components/stream-viewer.d.ts.map +1 -1
- package/dist/components/stream-viewer.js +3 -27
- package/dist/components/trace-viewer/components/map.js +1 -1
- package/dist/components/trace-viewer/components/markers.js +1 -1
- package/dist/components/trace-viewer/components/node.js +1 -1
- package/dist/components/trace-viewer/components/search-input.js +1 -1
- package/dist/components/trace-viewer/components/search.js +1 -1
- package/dist/components/trace-viewer/components/span-content.js +1 -1
- package/dist/components/trace-viewer/components/span-detail-panel.js +1 -1
- package/dist/components/trace-viewer/components/span-segments.js +1 -1
- package/dist/components/trace-viewer/components/span-strategies.js +1 -1
- package/dist/components/trace-viewer/components/ui.js +1 -1
- package/dist/components/trace-viewer/components/zoom-button.js +1 -1
- package/dist/components/trace-viewer/components/zoom-icons.js +1 -1
- package/dist/components/trace-viewer/context.js +1 -1
- package/dist/components/trace-viewer/index.js +1 -1
- package/dist/components/trace-viewer/trace-viewer.js +1 -1
- package/dist/components/trace-viewer/types.js +1 -1
- package/dist/components/trace-viewer/util/constants.js +1 -1
- package/dist/components/trace-viewer/util/scrollbar-width.js +1 -1
- package/dist/components/trace-viewer/util/timing.d.ts +1 -1
- package/dist/components/trace-viewer/util/timing.d.ts.map +1 -1
- package/dist/components/trace-viewer/util/timing.js +6 -9
- package/dist/components/trace-viewer/util/tree.js +1 -1
- package/dist/components/trace-viewer/util/use-immediate-style.js +1 -1
- package/dist/components/trace-viewer/util/use-streaming-spans.js +1 -1
- package/dist/components/trace-viewer/util/use-trackpad-zoom.js +1 -1
- package/dist/components/trace-viewer/worker.js +1 -1
- package/dist/components/trace-viewer-new.d.ts +10 -0
- package/dist/components/trace-viewer-new.d.ts.map +1 -0
- package/dist/components/trace-viewer-new.js +23 -0
- package/dist/components/ui/alert.js +1 -1
- package/dist/components/ui/button.d.ts +10 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/button.js +46 -0
- package/dist/components/ui/card.js +1 -1
- package/dist/components/ui/data-inspector.d.ts +39 -1
- package/dist/components/ui/data-inspector.d.ts.map +1 -1
- package/dist/components/ui/data-inspector.js +207 -22
- package/dist/components/ui/decrypt-button.js +1 -1
- package/dist/components/ui/error-card.js +1 -1
- package/dist/components/ui/error-stack-block.d.ts.map +1 -1
- package/dist/components/ui/error-stack-block.js +27 -19
- package/dist/components/ui/icon-button.d.ts +13 -0
- package/dist/components/ui/icon-button.d.ts.map +1 -0
- package/dist/components/ui/icon-button.js +29 -0
- package/dist/components/ui/inspector-theme.d.ts +9 -10
- package/dist/components/ui/inspector-theme.d.ts.map +1 -1
- package/dist/components/ui/inspector-theme.js +39 -72
- package/dist/components/ui/load-more-button.js +1 -1
- package/dist/components/ui/menu-dropdown.js +1 -1
- package/dist/components/ui/skeleton.js +1 -1
- package/dist/components/ui/spinner.js +1 -1
- package/dist/components/ui/timestamp-tooltip.js +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 +10 -5
- package/dist/components/workflow-traces/event-colors.js +1 -1
- package/dist/components/workflow-traces/trace-colors.js +1 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts +2 -2
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +19 -6
- package/dist/components/workflow-traces/trace-time-utils.js +1 -1
- package/dist/hooks/use-dark-mode.js +1 -1
- package/dist/hooks/use-reduced-motion.d.ts +10 -0
- package/dist/hooks/use-reduced-motion.d.ts.map +1 -0
- package/dist/hooks/use-reduced-motion.js +29 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/lib/event-analysis.js +1 -1
- package/dist/lib/event-materialization.js +1 -1
- package/dist/lib/exact-event-search-id.d.ts +25 -0
- package/dist/lib/exact-event-search-id.d.ts.map +1 -0
- package/dist/lib/exact-event-search-id.js +39 -0
- package/dist/lib/hydration.d.ts +14 -1
- package/dist/lib/hydration.d.ts.map +1 -1
- package/dist/lib/hydration.js +157 -4
- package/dist/lib/stream-display.d.ts +18 -0
- package/dist/lib/stream-display.d.ts.map +1 -0
- package/dist/lib/stream-display.js +30 -0
- package/dist/lib/toast.js +1 -1
- package/dist/lib/trace-builder.d.ts.map +1 -1
- package/dist/lib/trace-builder.js +3 -2
- package/dist/lib/utils.d.ts +9 -9
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +27 -32
- package/dist/styles.css +475 -1
- package/package.json +6 -4
- package/src/components/event-list-view.tsx +421 -262
- package/src/components/index.ts +12 -1
- package/src/components/new-trace-viewer/components/copy-button.tsx +68 -0
- package/src/components/new-trace-viewer/components/event-list.tsx +127 -0
- package/src/components/new-trace-viewer/components/middle-truncate/copy-selection.ts +114 -0
- package/src/components/new-trace-viewer/components/middle-truncate/middle-truncate.tsx +157 -0
- package/src/components/new-trace-viewer/components/middle-truncate/truncate.ts +132 -0
- package/src/components/new-trace-viewer/components/middle-truncate/use-middle-truncate.ts +167 -0
- package/src/components/new-trace-viewer/components/split-pane.tsx +185 -0
- package/src/components/new-trace-viewer/components/timeline.tsx +552 -0
- package/src/components/new-trace-viewer/context.tsx +92 -0
- package/src/components/new-trace-viewer/detail-panel.tsx +55 -0
- package/src/components/new-trace-viewer/icons.tsx +79 -0
- package/src/components/new-trace-viewer/search.ts +178 -0
- package/src/components/new-trace-viewer/trace-viewer.tsx +694 -0
- package/src/components/new-trace-viewer/types.ts +0 -0
- package/src/components/new-trace-viewer/util/constants.ts +0 -0
- package/src/components/new-trace-viewer/utils.ts +482 -0
- package/src/components/run-trace-view.tsx +3 -0
- package/src/components/sidebar/attribute-panel.tsx +240 -196
- package/src/components/sidebar/copyable-data-block.tsx +49 -27
- package/src/components/sidebar/detail-card.tsx +119 -59
- package/src/components/sidebar/entity-detail-panel.tsx +92 -148
- package/src/components/sidebar/events-list.tsx +51 -76
- package/src/components/sidebar/sidebar-data-context.tsx +62 -0
- package/src/components/stream-viewer.tsx +18 -40
- package/src/components/trace-viewer/util/timing.ts +5 -9
- package/src/components/trace-viewer-new.tsx +49 -0
- package/src/components/ui/button.tsx +68 -0
- package/src/components/ui/data-inspector.tsx +427 -31
- package/src/components/ui/error-stack-block.tsx +39 -30
- package/src/components/ui/icon-button.tsx +57 -0
- package/src/components/ui/inspector-theme.ts +39 -81
- package/src/components/workflow-trace-view.tsx +12 -2
- package/src/components/workflow-traces/trace-span-construction.ts +24 -6
- package/src/hooks/use-reduced-motion.ts +34 -0
- package/src/index.ts +9 -0
- package/src/lib/exact-event-search-id.ts +67 -0
- package/src/lib/hydration.ts +169 -1
- package/src/lib/stream-display.ts +48 -0
- package/src/lib/trace-builder.ts +2 -1
- package/src/lib/utils.ts +27 -32
- package/src/styles.css +475 -1
- package/dist/components/error-boundary.js.map +0 -1
- package/dist/components/event-list-view.js.map +0 -1
- package/dist/components/hook-actions.js.map +0 -1
- package/dist/components/index.js.map +0 -1
- package/dist/components/run-trace-view.js.map +0 -1
- package/dist/components/sidebar/attribute-panel.js.map +0 -1
- package/dist/components/sidebar/conversation-view.js.map +0 -1
- package/dist/components/sidebar/copyable-data-block.js.map +0 -1
- package/dist/components/sidebar/detail-card.js.map +0 -1
- package/dist/components/sidebar/entity-detail-panel.js.map +0 -1
- package/dist/components/sidebar/events-list.js.map +0 -1
- package/dist/components/sidebar/resolve-hook-modal.js.map +0 -1
- package/dist/components/stream-viewer.js.map +0 -1
- package/dist/components/trace-viewer/components/map.js.map +0 -1
- package/dist/components/trace-viewer/components/markers.js.map +0 -1
- package/dist/components/trace-viewer/components/node.js.map +0 -1
- package/dist/components/trace-viewer/components/search-input.js.map +0 -1
- package/dist/components/trace-viewer/components/search.js.map +0 -1
- package/dist/components/trace-viewer/components/span-content.js.map +0 -1
- package/dist/components/trace-viewer/components/span-detail-panel.js.map +0 -1
- package/dist/components/trace-viewer/components/span-segments.js.map +0 -1
- package/dist/components/trace-viewer/components/span-strategies.js.map +0 -1
- package/dist/components/trace-viewer/components/ui.js.map +0 -1
- package/dist/components/trace-viewer/components/zoom-button.js.map +0 -1
- package/dist/components/trace-viewer/components/zoom-icons.js.map +0 -1
- package/dist/components/trace-viewer/context.js.map +0 -1
- package/dist/components/trace-viewer/index.js.map +0 -1
- package/dist/components/trace-viewer/trace-viewer.js.map +0 -1
- package/dist/components/trace-viewer/types.js.map +0 -1
- package/dist/components/trace-viewer/util/constants.js.map +0 -1
- package/dist/components/trace-viewer/util/scrollbar-width.js.map +0 -1
- package/dist/components/trace-viewer/util/timing.js.map +0 -1
- package/dist/components/trace-viewer/util/tree.js.map +0 -1
- package/dist/components/trace-viewer/util/use-immediate-style.js.map +0 -1
- package/dist/components/trace-viewer/util/use-streaming-spans.js.map +0 -1
- package/dist/components/trace-viewer/util/use-trackpad-zoom.js.map +0 -1
- package/dist/components/trace-viewer/worker.js.map +0 -1
- package/dist/components/ui/alert.js.map +0 -1
- package/dist/components/ui/card.js.map +0 -1
- package/dist/components/ui/data-inspector.js.map +0 -1
- package/dist/components/ui/decrypt-button.js.map +0 -1
- package/dist/components/ui/error-card.js.map +0 -1
- package/dist/components/ui/error-stack-block.js.map +0 -1
- package/dist/components/ui/inspector-theme.js.map +0 -1
- package/dist/components/ui/load-more-button.js.map +0 -1
- package/dist/components/ui/menu-dropdown.js.map +0 -1
- package/dist/components/ui/skeleton.js.map +0 -1
- package/dist/components/ui/spinner.js.map +0 -1
- package/dist/components/ui/timestamp-tooltip.js.map +0 -1
- package/dist/components/workflow-trace-view.js.map +0 -1
- package/dist/components/workflow-traces/event-colors.js.map +0 -1
- package/dist/components/workflow-traces/trace-colors.js.map +0 -1
- package/dist/components/workflow-traces/trace-span-construction.js.map +0 -1
- package/dist/components/workflow-traces/trace-time-utils.js.map +0 -1
- package/dist/hooks/use-dark-mode.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/event-analysis.js.map +0 -1
- package/dist/lib/event-materialization.js.map +0 -1
- package/dist/lib/hydration.js.map +0 -1
- package/dist/lib/toast.js.map +0 -1
- package/dist/lib/trace-builder.js.map +0 -1
- package/dist/lib/utils.js.map +0 -1
|
@@ -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,
|
|
@@ -19,6 +26,11 @@ import {
|
|
|
19
26
|
} from 'react-inspector';
|
|
20
27
|
import { useDarkMode } from '../../hooks/use-dark-mode';
|
|
21
28
|
import { ENCRYPTED_DISPLAY_NAME } from '../../lib/hydration';
|
|
29
|
+
import {
|
|
30
|
+
type DecodedStreamChunkSource,
|
|
31
|
+
type FormattedStreamChunkDisplay,
|
|
32
|
+
formatArrayBufferViewForDisplay,
|
|
33
|
+
} from '../../lib/stream-display';
|
|
22
34
|
import {
|
|
23
35
|
type InspectorThemeExtended,
|
|
24
36
|
inspectorThemeDark,
|
|
@@ -26,6 +38,8 @@ import {
|
|
|
26
38
|
inspectorThemeExtendedLight,
|
|
27
39
|
inspectorThemeLight,
|
|
28
40
|
} from './inspector-theme';
|
|
41
|
+
import { Button } from './button';
|
|
42
|
+
import { Spinner } from './spinner';
|
|
29
43
|
|
|
30
44
|
// ---------------------------------------------------------------------------
|
|
31
45
|
// StreamRef / ClassInstanceRef type detection
|
|
@@ -34,19 +48,62 @@ import {
|
|
|
34
48
|
|
|
35
49
|
const STREAM_REF_TYPE = '__workflow_stream_ref__';
|
|
36
50
|
const CLASS_INSTANCE_REF_TYPE = '__workflow_class_instance_ref__';
|
|
51
|
+
const RUN_REF_TYPE = '__workflow_run_ref__';
|
|
52
|
+
const BYTES_DISPLAY_TYPE = '__workflow_bytes_display__';
|
|
37
53
|
|
|
38
54
|
interface StreamRef {
|
|
39
55
|
__type: typeof STREAM_REF_TYPE;
|
|
40
56
|
streamId: string;
|
|
41
57
|
}
|
|
42
58
|
|
|
59
|
+
interface RunRef {
|
|
60
|
+
__type: typeof RUN_REF_TYPE;
|
|
61
|
+
runId: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface BytesDisplay {
|
|
65
|
+
__type: typeof BYTES_DISPLAY_TYPE;
|
|
66
|
+
text: string;
|
|
67
|
+
decodedFrom?: DecodedStreamChunkSource;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function deserializeChunkText(text: string): string {
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(text);
|
|
73
|
+
if (typeof parsed === 'string') {
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
76
|
+
return JSON.stringify(parsed, null, 2);
|
|
77
|
+
} catch {
|
|
78
|
+
return text;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseChunkData(text: string): unknown {
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(text);
|
|
85
|
+
} catch {
|
|
86
|
+
return text;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
43
90
|
function isStreamRef(value: unknown): value is StreamRef {
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
91
|
+
if (value === null || typeof value !== 'object') return false;
|
|
92
|
+
// Check both enumerable and non-enumerable __type (opaque refs use non-enumerable)
|
|
93
|
+
const desc = Object.getOwnPropertyDescriptor(value, '__type');
|
|
94
|
+
return desc?.value === STREAM_REF_TYPE;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isRunRef(value: unknown): value is RunRef {
|
|
98
|
+
if (value === null || typeof value !== 'object') return false;
|
|
99
|
+
const desc = Object.getOwnPropertyDescriptor(value, '__type');
|
|
100
|
+
return desc?.value === RUN_REF_TYPE;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isBytesDisplay(value: unknown): value is BytesDisplay {
|
|
104
|
+
if (value === null || typeof value !== 'object') return false;
|
|
105
|
+
const desc = Object.getOwnPropertyDescriptor(value, '__type');
|
|
106
|
+
return desc?.value === BYTES_DISPLAY_TYPE;
|
|
50
107
|
}
|
|
51
108
|
|
|
52
109
|
function isClassInstanceRef(value: unknown): value is {
|
|
@@ -75,18 +132,79 @@ export const StreamClickContext = createContext<
|
|
|
75
132
|
((streamId: string) => void) | undefined
|
|
76
133
|
>(undefined);
|
|
77
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Context for passing a decrypt handler down to DataInspector instances.
|
|
137
|
+
* When provided, encrypted markers become clickable buttons that trigger decryption.
|
|
138
|
+
*/
|
|
139
|
+
export type DecryptClickContextValue = {
|
|
140
|
+
onDecrypt: () => void;
|
|
141
|
+
isDecrypting: boolean;
|
|
142
|
+
hasEncryptedData?: boolean;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const DecryptClickContext = createContext<
|
|
146
|
+
DecryptClickContextValue | undefined
|
|
147
|
+
>(undefined);
|
|
148
|
+
|
|
149
|
+
export const RunClickContext = createContext<
|
|
150
|
+
((runId: string) => void) | undefined
|
|
151
|
+
>(undefined);
|
|
152
|
+
|
|
153
|
+
function EncryptedInlineLabel() {
|
|
154
|
+
const ctx = useContext(DecryptClickContext);
|
|
155
|
+
if (ctx) {
|
|
156
|
+
return (
|
|
157
|
+
<Button
|
|
158
|
+
size="xs"
|
|
159
|
+
className="align-baseline gap-x-1"
|
|
160
|
+
disabled={ctx.isDecrypting}
|
|
161
|
+
onClick={(e) => {
|
|
162
|
+
e.stopPropagation();
|
|
163
|
+
ctx.onDecrypt();
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
{ctx.isDecrypting ? (
|
|
167
|
+
<Spinner size={10} />
|
|
168
|
+
) : (
|
|
169
|
+
<Lock className="h-3 w-3" />
|
|
170
|
+
)}
|
|
171
|
+
<span>Decrypt</span>
|
|
172
|
+
</Button>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return (
|
|
176
|
+
<span style={{ color: 'var(--ds-gray-600)', fontStyle: 'italic' }}>
|
|
177
|
+
<Lock
|
|
178
|
+
className="h-3 w-3"
|
|
179
|
+
style={{
|
|
180
|
+
display: 'inline',
|
|
181
|
+
verticalAlign: 'middle',
|
|
182
|
+
marginRight: '3px',
|
|
183
|
+
marginTop: '-1px',
|
|
184
|
+
}}
|
|
185
|
+
/>
|
|
186
|
+
Encrypted
|
|
187
|
+
</span>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
78
190
|
function StreamRefInline({ streamRef }: { streamRef: StreamRef }) {
|
|
79
191
|
const onStreamClick = useContext(StreamClickContext);
|
|
192
|
+
const [hovered, setHovered] = useState(false);
|
|
80
193
|
return (
|
|
81
194
|
<button
|
|
82
195
|
type="button"
|
|
83
|
-
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-mono cursor-pointer"
|
|
196
|
+
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
197
|
style={{
|
|
85
|
-
backgroundColor: 'var(--ds-blue-100)',
|
|
86
|
-
color: 'var(--ds-blue-
|
|
198
|
+
backgroundColor: hovered ? 'var(--ds-blue-200)' : 'var(--ds-blue-100)',
|
|
199
|
+
color: 'var(--ds-blue-900)',
|
|
87
200
|
border: '1px solid var(--ds-blue-300)',
|
|
88
201
|
}}
|
|
89
|
-
|
|
202
|
+
onMouseEnter={() => setHovered(true)}
|
|
203
|
+
onMouseLeave={() => setHovered(false)}
|
|
204
|
+
onClick={(e) => {
|
|
205
|
+
e.stopPropagation();
|
|
206
|
+
onStreamClick?.(streamRef.streamId);
|
|
207
|
+
}}
|
|
90
208
|
title={`View stream: ${streamRef.streamId}`}
|
|
91
209
|
>
|
|
92
210
|
<span>📡</span>
|
|
@@ -95,6 +213,33 @@ function StreamRefInline({ streamRef }: { streamRef: StreamRef }) {
|
|
|
95
213
|
);
|
|
96
214
|
}
|
|
97
215
|
|
|
216
|
+
function RunRefInline({ runRef }: { runRef: RunRef }) {
|
|
217
|
+
const onRunClick = useContext(RunClickContext);
|
|
218
|
+
const [hovered, setHovered] = useState(false);
|
|
219
|
+
return (
|
|
220
|
+
<button
|
|
221
|
+
type="button"
|
|
222
|
+
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"
|
|
223
|
+
style={{
|
|
224
|
+
backgroundColor: hovered
|
|
225
|
+
? 'var(--ds-purple-200)'
|
|
226
|
+
: 'var(--ds-purple-100)',
|
|
227
|
+
color: 'var(--ds-purple-900)',
|
|
228
|
+
border: '1px solid var(--ds-purple-300)',
|
|
229
|
+
}}
|
|
230
|
+
onMouseEnter={() => setHovered(true)}
|
|
231
|
+
onMouseLeave={() => setHovered(false)}
|
|
232
|
+
onClick={(e) => {
|
|
233
|
+
e.stopPropagation();
|
|
234
|
+
onRunClick?.(runRef.runId);
|
|
235
|
+
}}
|
|
236
|
+
title={`View run: ${runRef.runId}`}
|
|
237
|
+
>
|
|
238
|
+
<span>{runRef.runId}</span>
|
|
239
|
+
</button>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
98
243
|
// ---------------------------------------------------------------------------
|
|
99
244
|
// Extended theme context (for colors react-inspector doesn't support natively)
|
|
100
245
|
// ---------------------------------------------------------------------------
|
|
@@ -103,6 +248,147 @@ const ExtendedThemeContext = createContext<InspectorThemeExtended>(
|
|
|
103
248
|
inspectorThemeExtendedLight
|
|
104
249
|
);
|
|
105
250
|
|
|
251
|
+
function DecodedBytesChunk({
|
|
252
|
+
decodedText,
|
|
253
|
+
source,
|
|
254
|
+
}: {
|
|
255
|
+
decodedText: string;
|
|
256
|
+
source: DecodedStreamChunkSource;
|
|
257
|
+
}) {
|
|
258
|
+
const [selectedView, setSelectedView] = useState<'decoded' | 'bytes'>(
|
|
259
|
+
'decoded'
|
|
260
|
+
);
|
|
261
|
+
const parsed = parseChunkData(decodedText);
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<div className="min-w-0">
|
|
265
|
+
{selectedView === 'decoded' ? (
|
|
266
|
+
<div className="min-w-0">
|
|
267
|
+
{typeof parsed === 'string' ? (
|
|
268
|
+
<span
|
|
269
|
+
className="whitespace-pre-wrap break-words"
|
|
270
|
+
style={{ color: 'var(--ds-gray-1000)' }}
|
|
271
|
+
>
|
|
272
|
+
{deserializeChunkText(parsed)}
|
|
273
|
+
</span>
|
|
274
|
+
) : (
|
|
275
|
+
<DataInspector data={parsed} expandLevel={1} />
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
) : (
|
|
279
|
+
<DecodedBytesInspector decodedText={decodedText} source={source} />
|
|
280
|
+
)}
|
|
281
|
+
<div className="mt-2 flex">
|
|
282
|
+
<div
|
|
283
|
+
className="inline-flex overflow-hidden rounded border"
|
|
284
|
+
style={{ borderColor: 'var(--ds-gray-400)' }}
|
|
285
|
+
title={`${source.type} decoded as ${source.encoding.toUpperCase()} text. Switch to Bytes to inspect the summarized raw value.`}
|
|
286
|
+
>
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
className="h-5 px-1.5 text-[10px] font-medium"
|
|
290
|
+
style={{
|
|
291
|
+
backgroundColor:
|
|
292
|
+
selectedView === 'decoded'
|
|
293
|
+
? 'var(--ds-gray-200)'
|
|
294
|
+
: 'var(--ds-gray-100)',
|
|
295
|
+
color: 'var(--ds-gray-900)',
|
|
296
|
+
}}
|
|
297
|
+
onClick={() => setSelectedView('decoded')}
|
|
298
|
+
aria-pressed={selectedView === 'decoded'}
|
|
299
|
+
aria-label="Show decoded text"
|
|
300
|
+
>
|
|
301
|
+
Decoded
|
|
302
|
+
</button>
|
|
303
|
+
<button
|
|
304
|
+
type="button"
|
|
305
|
+
className="h-5 border-l px-1.5 text-[10px] font-medium"
|
|
306
|
+
style={{
|
|
307
|
+
borderColor: 'var(--ds-gray-400)',
|
|
308
|
+
backgroundColor:
|
|
309
|
+
selectedView === 'bytes'
|
|
310
|
+
? 'var(--ds-gray-200)'
|
|
311
|
+
: 'var(--ds-gray-100)',
|
|
312
|
+
color: 'var(--ds-gray-900)',
|
|
313
|
+
}}
|
|
314
|
+
onClick={() => setSelectedView('bytes')}
|
|
315
|
+
aria-pressed={selectedView === 'bytes'}
|
|
316
|
+
aria-label="Show raw bytes summary"
|
|
317
|
+
>
|
|
318
|
+
Bytes
|
|
319
|
+
</button>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function DecodedBytesInspector({
|
|
327
|
+
decodedText,
|
|
328
|
+
source,
|
|
329
|
+
}: {
|
|
330
|
+
decodedText: string;
|
|
331
|
+
source: DecodedStreamChunkSource;
|
|
332
|
+
}) {
|
|
333
|
+
const [expanded, setExpanded] = useState(false);
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<div className="font-mono">
|
|
337
|
+
<button
|
|
338
|
+
type="button"
|
|
339
|
+
className="flex max-w-full items-start gap-1 text-left"
|
|
340
|
+
style={{ color: 'var(--ds-gray-1000)' }}
|
|
341
|
+
onClick={() => setExpanded((value) => !value)}
|
|
342
|
+
title={`${source.type} decoded as ${source.encoding.toUpperCase()} text`}
|
|
343
|
+
>
|
|
344
|
+
<span className="select-none" style={{ color: 'var(--ds-gray-700)' }}>
|
|
345
|
+
{expanded ? '▼' : '▶'}
|
|
346
|
+
</span>
|
|
347
|
+
<span className="min-w-0 break-words">{source.rawSummary}</span>
|
|
348
|
+
</button>
|
|
349
|
+
{expanded && (
|
|
350
|
+
<div className="mt-1 pl-5">
|
|
351
|
+
<span style={{ color: 'var(--ds-gray-700)' }}>decoded: </span>
|
|
352
|
+
<span
|
|
353
|
+
className="whitespace-pre-wrap break-words"
|
|
354
|
+
style={{ color: 'var(--ds-green-900)' }}
|
|
355
|
+
>
|
|
356
|
+
{JSON.stringify(decodedText)}
|
|
357
|
+
</span>
|
|
358
|
+
</div>
|
|
359
|
+
)}
|
|
360
|
+
</div>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function BytesDisplayLabel({
|
|
365
|
+
name,
|
|
366
|
+
display,
|
|
367
|
+
}: {
|
|
368
|
+
name?: string;
|
|
369
|
+
display: BytesDisplay;
|
|
370
|
+
}) {
|
|
371
|
+
return (
|
|
372
|
+
<div className="inline-block min-w-0 align-top">
|
|
373
|
+
{name != null && <ObjectName name={name} />}
|
|
374
|
+
{name != null && <span>: </span>}
|
|
375
|
+
{display.decodedFrom ? (
|
|
376
|
+
<DecodedBytesChunk
|
|
377
|
+
decodedText={display.text}
|
|
378
|
+
source={display.decodedFrom}
|
|
379
|
+
/>
|
|
380
|
+
) : (
|
|
381
|
+
<span
|
|
382
|
+
className="whitespace-pre-wrap break-words"
|
|
383
|
+
style={{ color: 'var(--ds-gray-1000)' }}
|
|
384
|
+
>
|
|
385
|
+
{display.text}
|
|
386
|
+
</span>
|
|
387
|
+
)}
|
|
388
|
+
</div>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
106
392
|
// ---------------------------------------------------------------------------
|
|
107
393
|
// Custom nodeRenderer
|
|
108
394
|
// ---------------------------------------------------------------------------
|
|
@@ -132,26 +418,17 @@ function NodeRenderer({
|
|
|
132
418
|
}) {
|
|
133
419
|
const extendedTheme = useContext(ExtendedThemeContext);
|
|
134
420
|
|
|
135
|
-
|
|
421
|
+
if (isBytesDisplay(data)) {
|
|
422
|
+
return <BytesDisplayLabel name={name} display={data} />;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Encrypted marker → flat label with Lock icon, clickable when onDecrypt is available
|
|
136
426
|
if (
|
|
137
427
|
data !== null &&
|
|
138
428
|
typeof data === 'object' &&
|
|
139
429
|
data.constructor?.name === ENCRYPTED_DISPLAY_NAME
|
|
140
430
|
) {
|
|
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
|
-
);
|
|
431
|
+
const label = <EncryptedInlineLabel />;
|
|
155
432
|
if (depth === 0) {
|
|
156
433
|
return label;
|
|
157
434
|
}
|
|
@@ -175,6 +452,17 @@ function NodeRenderer({
|
|
|
175
452
|
);
|
|
176
453
|
}
|
|
177
454
|
|
|
455
|
+
// RunRef → inline clickable badge linking to the target run
|
|
456
|
+
if (isRunRef(data)) {
|
|
457
|
+
return (
|
|
458
|
+
<span>
|
|
459
|
+
{name != null && <ObjectName name={name} />}
|
|
460
|
+
{name != null && <span>: </span>}
|
|
461
|
+
<RunRefInline runRef={data} />
|
|
462
|
+
</span>
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
178
466
|
// ClassInstanceRef → show className as type, data as the inspectable value
|
|
179
467
|
if (isClassInstanceRef(data)) {
|
|
180
468
|
if (depth === 0) {
|
|
@@ -225,6 +513,76 @@ function NodeRenderer({
|
|
|
225
513
|
// Public component
|
|
226
514
|
// ---------------------------------------------------------------------------
|
|
227
515
|
|
|
516
|
+
/**
|
|
517
|
+
* Create a non-expandable wrapper that carries ref data as non-enumerable
|
|
518
|
+
* properties. ObjectInspector won't render children for objects with no
|
|
519
|
+
* enumerable keys, but our NodeRenderer can still detect them.
|
|
520
|
+
*/
|
|
521
|
+
function makeOpaqueRef(ref: Record<string, unknown>): unknown {
|
|
522
|
+
const opaque = Object.create(null);
|
|
523
|
+
for (const [key, value] of Object.entries(ref)) {
|
|
524
|
+
Object.defineProperty(opaque, key, { value, enumerable: false });
|
|
525
|
+
}
|
|
526
|
+
return opaque;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function makeBytesDisplay(display: FormattedStreamChunkDisplay): unknown {
|
|
530
|
+
const opaque = Object.create(null);
|
|
531
|
+
Object.defineProperty(opaque, '__type', {
|
|
532
|
+
value: BYTES_DISPLAY_TYPE,
|
|
533
|
+
enumerable: false,
|
|
534
|
+
});
|
|
535
|
+
Object.defineProperty(opaque, 'text', {
|
|
536
|
+
value: display.text,
|
|
537
|
+
enumerable: false,
|
|
538
|
+
});
|
|
539
|
+
Object.defineProperty(opaque, 'decodedFrom', {
|
|
540
|
+
value: display.decodedFrom,
|
|
541
|
+
enumerable: false,
|
|
542
|
+
});
|
|
543
|
+
return opaque;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Recursively walk data and replace RunRef/StreamRef/typed array objects with
|
|
548
|
+
* non-expandable versions so ObjectInspector doesn't show their internals.
|
|
549
|
+
* Only recurses into plain objects and arrays to avoid stripping class
|
|
550
|
+
* instances (Date, Error, URL, Headers, etc.) that have their own rendering in
|
|
551
|
+
* NodeRenderer. Map and Set containers are preserved while their contents are
|
|
552
|
+
* prepared for display.
|
|
553
|
+
*
|
|
554
|
+
* Exported for testing the typed-array detection path used by hydrated
|
|
555
|
+
* AI agent stream chunks (e.g. `{ delta: new Uint8Array(...) }`).
|
|
556
|
+
*/
|
|
557
|
+
export function collapseRefs(data: unknown): unknown {
|
|
558
|
+
if (data === null || typeof data !== 'object') return data;
|
|
559
|
+
if (ArrayBuffer.isView(data) && !(data instanceof DataView)) {
|
|
560
|
+
return makeBytesDisplay(formatArrayBufferViewForDisplay(data));
|
|
561
|
+
}
|
|
562
|
+
if (isRunRef(data) || isStreamRef(data))
|
|
563
|
+
return makeOpaqueRef(data as unknown as Record<string, unknown>);
|
|
564
|
+
if (Array.isArray(data)) return data.map(collapseRefs);
|
|
565
|
+
if (data instanceof Map) {
|
|
566
|
+
return new Map(
|
|
567
|
+
Array.from(data.entries(), ([key, value]) => [
|
|
568
|
+
collapseRefs(key),
|
|
569
|
+
collapseRefs(value),
|
|
570
|
+
])
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
if (data instanceof Set) {
|
|
574
|
+
return new Set(Array.from(data.values(), collapseRefs));
|
|
575
|
+
}
|
|
576
|
+
// Only recurse into plain objects — leave class instances untouched
|
|
577
|
+
const proto = Object.getPrototypeOf(data);
|
|
578
|
+
if (proto !== Object.prototype && proto !== null) return data;
|
|
579
|
+
const result: Record<string, unknown> = {};
|
|
580
|
+
for (const [key, value] of Object.entries(data)) {
|
|
581
|
+
result[key] = collapseRefs(value);
|
|
582
|
+
}
|
|
583
|
+
return result;
|
|
584
|
+
}
|
|
585
|
+
|
|
228
586
|
export interface DataInspectorProps {
|
|
229
587
|
/** The data to inspect */
|
|
230
588
|
data: unknown;
|
|
@@ -234,6 +592,12 @@ export interface DataInspectorProps {
|
|
|
234
592
|
name?: string;
|
|
235
593
|
/** Callback when a stream reference is clicked */
|
|
236
594
|
onStreamClick?: (streamId: string) => void;
|
|
595
|
+
/** Callback when a run reference is clicked */
|
|
596
|
+
onRunClick?: (runId: string) => void;
|
|
597
|
+
/** Callback when an encrypted marker is clicked (triggers decryption) */
|
|
598
|
+
onDecrypt?: () => void;
|
|
599
|
+
/** Whether decryption is currently in progress */
|
|
600
|
+
isDecrypting?: boolean;
|
|
237
601
|
}
|
|
238
602
|
|
|
239
603
|
export function DataInspector({
|
|
@@ -241,8 +605,12 @@ export function DataInspector({
|
|
|
241
605
|
expandLevel = 2,
|
|
242
606
|
name,
|
|
243
607
|
onStreamClick,
|
|
608
|
+
onRunClick,
|
|
609
|
+
onDecrypt,
|
|
610
|
+
isDecrypting = false,
|
|
244
611
|
}: DataInspectorProps) {
|
|
245
|
-
const
|
|
612
|
+
const collapsedData = useMemo(() => collapseRefs(data), [data]);
|
|
613
|
+
const stableData = useStableInspectorData(collapsedData);
|
|
246
614
|
const [initialExpandLevel, setInitialExpandLevel] = useState(expandLevel);
|
|
247
615
|
const isDark = useDarkMode();
|
|
248
616
|
const extendedTheme = isDark
|
|
@@ -269,16 +637,31 @@ export function DataInspector({
|
|
|
269
637
|
</ExtendedThemeContext.Provider>
|
|
270
638
|
);
|
|
271
639
|
|
|
272
|
-
|
|
640
|
+
let wrapped = content;
|
|
641
|
+
|
|
273
642
|
if (onStreamClick) {
|
|
274
|
-
|
|
643
|
+
wrapped = (
|
|
275
644
|
<StreamClickContext.Provider value={onStreamClick}>
|
|
276
|
-
{
|
|
645
|
+
{wrapped}
|
|
277
646
|
</StreamClickContext.Provider>
|
|
278
647
|
);
|
|
279
648
|
}
|
|
649
|
+
if (onRunClick) {
|
|
650
|
+
wrapped = (
|
|
651
|
+
<RunClickContext.Provider value={onRunClick}>
|
|
652
|
+
{wrapped}
|
|
653
|
+
</RunClickContext.Provider>
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
if (onDecrypt) {
|
|
657
|
+
wrapped = (
|
|
658
|
+
<DecryptClickContext.Provider value={{ onDecrypt, isDecrypting }}>
|
|
659
|
+
{wrapped}
|
|
660
|
+
</DecryptClickContext.Provider>
|
|
661
|
+
);
|
|
662
|
+
}
|
|
280
663
|
|
|
281
|
-
return
|
|
664
|
+
return wrapped;
|
|
282
665
|
}
|
|
283
666
|
|
|
284
667
|
function useStableInspectorData<T>(next: T): T {
|
|
@@ -293,9 +676,22 @@ function isObjectLike(value: unknown): value is Record<string, unknown> {
|
|
|
293
676
|
return typeof value === 'object' && value !== null;
|
|
294
677
|
}
|
|
295
678
|
|
|
679
|
+
function isSameBytesDisplay(a: BytesDisplay, b: BytesDisplay): boolean {
|
|
680
|
+
return (
|
|
681
|
+
a.text === b.text &&
|
|
682
|
+
a.decodedFrom?.type === b.decodedFrom?.type &&
|
|
683
|
+
a.decodedFrom?.encoding === b.decodedFrom?.encoding &&
|
|
684
|
+
a.decodedFrom?.rawSummary === b.decodedFrom?.rawSummary
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
|
|
296
688
|
function isDeepEqual(a: unknown, b: unknown, seen = new WeakMap()): boolean {
|
|
297
689
|
if (Object.is(a, b)) return true;
|
|
298
690
|
|
|
691
|
+
if (isBytesDisplay(a) || isBytesDisplay(b)) {
|
|
692
|
+
return isBytesDisplay(a) && isBytesDisplay(b) && isSameBytesDisplay(a, b);
|
|
693
|
+
}
|
|
694
|
+
|
|
299
695
|
if (a instanceof Date && b instanceof Date) {
|
|
300
696
|
return a.getTime() === b.getTime();
|
|
301
697
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { AlertCircle
|
|
4
|
-
import {
|
|
3
|
+
import { AlertCircle } from 'lucide-react';
|
|
4
|
+
import { CopyButton } from '../new-trace-viewer/components/copy-button';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Check whether `value` looks like a structured error object with a `stack`
|
|
@@ -18,6 +18,21 @@ export function isStructuredErrorWithStack(
|
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Pull a short, single-line title out of an error message.
|
|
23
|
+
*
|
|
24
|
+
* Workflow's structured error messages are multi-line — the first line is
|
|
25
|
+
* the headline (`Failed to serialize step return value`) and the rest are
|
|
26
|
+
* `╰▶ hint:` / `╰▶ docs:` framed details. The full message belongs in the
|
|
27
|
+
* body of the error block; the title should just be the headline so the
|
|
28
|
+
* card stays scannable.
|
|
29
|
+
*/
|
|
30
|
+
function deriveTitle(message: string): string {
|
|
31
|
+
const firstLine =
|
|
32
|
+
message.split('\n').find((line) => line.trim().length > 0) ?? message;
|
|
33
|
+
return firstLine.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
/**
|
|
22
37
|
* Renders an error with a `stack` field as a visually distinct error block.
|
|
23
38
|
* Shows the error message with an alert icon at the top, separated from
|
|
@@ -28,9 +43,12 @@ export function ErrorStackBlock({
|
|
|
28
43
|
}: {
|
|
29
44
|
value: Record<string, unknown> & { stack: string };
|
|
30
45
|
}) {
|
|
31
|
-
const toast = useToast();
|
|
32
46
|
const stack = value.stack;
|
|
33
47
|
const message = typeof value.message === 'string' ? value.message : undefined;
|
|
48
|
+
// V8's `Error.stack` already starts with `Name: message`, so when the
|
|
49
|
+
// body shows the stack it includes the full multi-line message anyway.
|
|
50
|
+
// The header just needs the first line.
|
|
51
|
+
const title = message ? deriveTitle(message) : undefined;
|
|
34
52
|
const copyText = message ? `${message}\n\n${stack}` : stack;
|
|
35
53
|
|
|
36
54
|
return (
|
|
@@ -41,40 +59,31 @@ export function ErrorStackBlock({
|
|
|
41
59
|
background: 'var(--ds-red-100)',
|
|
42
60
|
}}
|
|
43
61
|
>
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
style={{
|
|
50
|
-
borderColor: 'var(--ds-red-400)',
|
|
51
|
-
background: 'var(--ds-red-100)',
|
|
52
|
-
color: 'var(--ds-red-900)',
|
|
53
|
-
}}
|
|
54
|
-
onClick={() => {
|
|
55
|
-
navigator.clipboard
|
|
56
|
-
.writeText(copyText)
|
|
57
|
-
.then(() => {
|
|
58
|
-
toast.success('Copied to clipboard');
|
|
59
|
-
})
|
|
60
|
-
.catch(() => {
|
|
61
|
-
toast.error('Failed to copy');
|
|
62
|
-
});
|
|
63
|
-
}}
|
|
64
|
-
>
|
|
65
|
-
<Copy size={12} />
|
|
66
|
-
</button>
|
|
62
|
+
<CopyButton
|
|
63
|
+
copyText={copyText}
|
|
64
|
+
ariaLabel="Copy error"
|
|
65
|
+
className="absolute right-2 top-2 flex h-6 w-6 items-center justify-center rounded-md border border-red-400 bg-red-100 p-0 text-red-900 transition-transform transition-colors duration-100 hover:bg-red-200 active:scale-95"
|
|
66
|
+
/>
|
|
67
67
|
|
|
68
|
-
{
|
|
68
|
+
{title && (
|
|
69
69
|
<div
|
|
70
|
-
className="flex items-
|
|
70
|
+
className="flex items-center gap-2 px-3 py-2.5 pr-10"
|
|
71
71
|
style={{
|
|
72
72
|
color: 'var(--ds-red-900)',
|
|
73
73
|
borderBottom: '1px solid var(--ds-red-400)',
|
|
74
74
|
}}
|
|
75
75
|
>
|
|
76
|
-
<AlertCircle className="h-4 w-4 shrink-0"
|
|
77
|
-
<p
|
|
76
|
+
<AlertCircle className="h-4 w-4 shrink-0" />
|
|
77
|
+
<p
|
|
78
|
+
className="text-xs font-semibold m-0 truncate"
|
|
79
|
+
// The full multi-line message is in the stack body below; the
|
|
80
|
+
// header just shows the first line, single-line, with overflow
|
|
81
|
+
// ellipsised so a long title doesn't push the copy button or
|
|
82
|
+
// wrap into the framed hint/docs lines.
|
|
83
|
+
title={message}
|
|
84
|
+
>
|
|
85
|
+
{title}
|
|
86
|
+
</p>
|
|
78
87
|
</div>
|
|
79
88
|
)}
|
|
80
89
|
<pre
|