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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/README.md +1 -1
  2. package/dist/components/error-boundary.js.map +1 -1
  3. package/dist/components/event-list-view.d.ts +3 -1
  4. package/dist/components/event-list-view.d.ts.map +1 -1
  5. package/dist/components/event-list-view.js +53 -53
  6. package/dist/components/event-list-view.js.map +1 -1
  7. package/dist/components/hook-actions.js.map +1 -1
  8. package/dist/components/index.d.ts +3 -1
  9. package/dist/components/index.d.ts.map +1 -1
  10. package/dist/components/index.js +3 -1
  11. package/dist/components/index.js.map +1 -1
  12. package/dist/components/new-trace-viewer/components/copy-button.d.ts +9 -0
  13. package/dist/components/new-trace-viewer/components/copy-button.d.ts.map +1 -0
  14. package/dist/components/new-trace-viewer/components/copy-button.js +27 -0
  15. package/dist/components/new-trace-viewer/components/copy-button.js.map +1 -0
  16. package/dist/components/new-trace-viewer/components/event-list.d.ts +8 -0
  17. package/dist/components/new-trace-viewer/components/event-list.d.ts.map +1 -0
  18. package/dist/components/new-trace-viewer/components/event-list.js +35 -0
  19. package/dist/components/new-trace-viewer/components/event-list.js.map +1 -0
  20. package/dist/components/new-trace-viewer/components/split-pane.d.ts +14 -0
  21. package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -0
  22. package/dist/components/new-trace-viewer/components/split-pane.js +100 -0
  23. package/dist/components/new-trace-viewer/components/split-pane.js.map +1 -0
  24. package/dist/components/new-trace-viewer/components/timeline.d.ts +32 -0
  25. package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -0
  26. package/dist/components/new-trace-viewer/components/timeline.js +134 -0
  27. package/dist/components/new-trace-viewer/components/timeline.js.map +1 -0
  28. package/dist/components/new-trace-viewer/context.d.ts +15 -0
  29. package/dist/components/new-trace-viewer/context.d.ts.map +1 -0
  30. package/dist/components/new-trace-viewer/context.js +49 -0
  31. package/dist/components/new-trace-viewer/context.js.map +1 -0
  32. package/dist/components/new-trace-viewer/detail-panel.d.ts +10 -0
  33. package/dist/components/new-trace-viewer/detail-panel.d.ts.map +1 -0
  34. package/dist/components/new-trace-viewer/detail-panel.js +11 -0
  35. package/dist/components/new-trace-viewer/detail-panel.js.map +1 -0
  36. package/dist/components/new-trace-viewer/icons.d.ts +6 -0
  37. package/dist/components/new-trace-viewer/icons.d.ts.map +1 -0
  38. package/dist/components/new-trace-viewer/icons.js +15 -0
  39. package/dist/components/new-trace-viewer/icons.js.map +1 -0
  40. package/dist/components/new-trace-viewer/trace-viewer.d.ts +8 -0
  41. package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -0
  42. package/dist/components/new-trace-viewer/trace-viewer.js +329 -0
  43. package/dist/components/new-trace-viewer/trace-viewer.js.map +1 -0
  44. package/dist/components/new-trace-viewer/types.d.ts +1 -0
  45. package/dist/components/new-trace-viewer/types.d.ts.map +1 -0
  46. package/dist/components/new-trace-viewer/types.js +2 -0
  47. package/dist/components/new-trace-viewer/types.js.map +1 -0
  48. package/dist/components/new-trace-viewer/util/constants.d.ts +1 -0
  49. package/dist/components/new-trace-viewer/util/constants.d.ts.map +1 -0
  50. package/dist/components/new-trace-viewer/util/constants.js +2 -0
  51. package/dist/components/new-trace-viewer/util/constants.js.map +1 -0
  52. package/dist/components/new-trace-viewer/utils.d.ts +47 -0
  53. package/dist/components/new-trace-viewer/utils.d.ts.map +1 -0
  54. package/dist/components/new-trace-viewer/utils.js +358 -0
  55. package/dist/components/new-trace-viewer/utils.js.map +1 -0
  56. package/dist/components/run-trace-view.d.ts +2 -1
  57. package/dist/components/run-trace-view.d.ts.map +1 -1
  58. package/dist/components/run-trace-view.js +2 -2
  59. package/dist/components/run-trace-view.js.map +1 -1
  60. package/dist/components/sidebar/attribute-panel.d.ts +7 -1
  61. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  62. package/dist/components/sidebar/attribute-panel.js +76 -47
  63. package/dist/components/sidebar/attribute-panel.js.map +1 -1
  64. package/dist/components/sidebar/conversation-view.js.map +1 -1
  65. package/dist/components/sidebar/copyable-data-block.js +1 -1
  66. package/dist/components/sidebar/copyable-data-block.js.map +1 -1
  67. package/dist/components/sidebar/detail-card.d.ts.map +1 -1
  68. package/dist/components/sidebar/detail-card.js +1 -1
  69. package/dist/components/sidebar/detail-card.js.map +1 -1
  70. package/dist/components/sidebar/entity-detail-panel.d.ts +3 -1
  71. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  72. package/dist/components/sidebar/entity-detail-panel.js +16 -36
  73. package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
  74. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  75. package/dist/components/sidebar/events-list.js +2 -2
  76. package/dist/components/sidebar/events-list.js.map +1 -1
  77. package/dist/components/sidebar/resolve-hook-modal.js.map +1 -1
  78. package/dist/components/sidebar/sidebar-data-context.d.ts +29 -0
  79. package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -0
  80. package/dist/components/sidebar/sidebar-data-context.js +19 -0
  81. package/dist/components/sidebar/sidebar-data-context.js.map +1 -0
  82. package/dist/components/stream-viewer.d.ts +1 -1
  83. package/dist/components/stream-viewer.d.ts.map +1 -1
  84. package/dist/components/stream-viewer.js +2 -26
  85. package/dist/components/stream-viewer.js.map +1 -1
  86. package/dist/components/trace-viewer/components/map.js.map +1 -1
  87. package/dist/components/trace-viewer/components/markers.js.map +1 -1
  88. package/dist/components/trace-viewer/components/node.js.map +1 -1
  89. package/dist/components/trace-viewer/components/search-input.js.map +1 -1
  90. package/dist/components/trace-viewer/components/search.js.map +1 -1
  91. package/dist/components/trace-viewer/components/span-content.js.map +1 -1
  92. package/dist/components/trace-viewer/components/span-detail-panel.js.map +1 -1
  93. package/dist/components/trace-viewer/components/span-segments.js.map +1 -1
  94. package/dist/components/trace-viewer/components/span-strategies.js.map +1 -1
  95. package/dist/components/trace-viewer/components/ui.js.map +1 -1
  96. package/dist/components/trace-viewer/components/zoom-button.js.map +1 -1
  97. package/dist/components/trace-viewer/components/zoom-icons.js.map +1 -1
  98. package/dist/components/trace-viewer/context.js.map +1 -1
  99. package/dist/components/trace-viewer/index.js.map +1 -1
  100. package/dist/components/trace-viewer/trace-viewer.js.map +1 -1
  101. package/dist/components/trace-viewer/types.js.map +1 -1
  102. package/dist/components/trace-viewer/util/constants.js.map +1 -1
  103. package/dist/components/trace-viewer/util/scrollbar-width.js.map +1 -1
  104. package/dist/components/trace-viewer/util/timing.js.map +1 -1
  105. package/dist/components/trace-viewer/util/tree.js.map +1 -1
  106. package/dist/components/trace-viewer/util/use-immediate-style.js.map +1 -1
  107. package/dist/components/trace-viewer/util/use-streaming-spans.js.map +1 -1
  108. package/dist/components/trace-viewer/util/use-trackpad-zoom.js.map +1 -1
  109. package/dist/components/trace-viewer/worker.js.map +1 -1
  110. package/dist/components/trace-viewer-new.d.ts +10 -0
  111. package/dist/components/trace-viewer-new.d.ts.map +1 -0
  112. package/dist/components/trace-viewer-new.js +23 -0
  113. package/dist/components/trace-viewer-new.js.map +1 -0
  114. package/dist/components/ui/alert.js.map +1 -1
  115. package/dist/components/ui/card.js.map +1 -1
  116. package/dist/components/ui/data-inspector.d.ts +38 -1
  117. package/dist/components/ui/data-inspector.d.ts.map +1 -1
  118. package/dist/components/ui/data-inspector.js +211 -21
  119. package/dist/components/ui/data-inspector.js.map +1 -1
  120. package/dist/components/ui/decrypt-button.js.map +1 -1
  121. package/dist/components/ui/error-card.js.map +1 -1
  122. package/dist/components/ui/error-stack-block.js.map +1 -1
  123. package/dist/components/ui/inspector-theme.d.ts +9 -10
  124. package/dist/components/ui/inspector-theme.d.ts.map +1 -1
  125. package/dist/components/ui/inspector-theme.js +38 -71
  126. package/dist/components/ui/inspector-theme.js.map +1 -1
  127. package/dist/components/ui/load-more-button.js.map +1 -1
  128. package/dist/components/ui/menu-dropdown.js.map +1 -1
  129. package/dist/components/ui/skeleton.js.map +1 -1
  130. package/dist/components/ui/spinner.js.map +1 -1
  131. package/dist/components/ui/timestamp-tooltip.js.map +1 -1
  132. package/dist/components/workflow-trace-view.d.ts +3 -1
  133. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  134. package/dist/components/workflow-trace-view.js +9 -4
  135. package/dist/components/workflow-trace-view.js.map +1 -1
  136. package/dist/components/workflow-traces/event-colors.js.map +1 -1
  137. package/dist/components/workflow-traces/trace-colors.js.map +1 -1
  138. package/dist/components/workflow-traces/trace-span-construction.js +1 -1
  139. package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
  140. package/dist/components/workflow-traces/trace-time-utils.js.map +1 -1
  141. package/dist/hooks/use-dark-mode.js.map +1 -1
  142. package/dist/index.d.ts +2 -1
  143. package/dist/index.d.ts.map +1 -1
  144. package/dist/index.js +1 -1
  145. package/dist/index.js.map +1 -1
  146. package/dist/lib/event-analysis.js.map +1 -1
  147. package/dist/lib/event-materialization.js.map +1 -1
  148. package/dist/lib/hydration.d.ts +8 -1
  149. package/dist/lib/hydration.d.ts.map +1 -1
  150. package/dist/lib/hydration.js +32 -2
  151. package/dist/lib/hydration.js.map +1 -1
  152. package/dist/lib/stream-display.d.ts +18 -0
  153. package/dist/lib/stream-display.d.ts.map +1 -0
  154. package/dist/lib/stream-display.js +30 -0
  155. package/dist/lib/stream-display.js.map +1 -0
  156. package/dist/lib/toast.js.map +1 -1
  157. package/dist/lib/trace-builder.js.map +1 -1
  158. package/dist/lib/utils.d.ts +7 -9
  159. package/dist/lib/utils.d.ts.map +1 -1
  160. package/dist/lib/utils.js +17 -22
  161. package/dist/lib/utils.js.map +1 -1
  162. package/dist/styles.css +470 -1
  163. package/package.json +4 -3
  164. package/src/components/event-list-view.tsx +181 -173
  165. package/src/components/index.ts +11 -1
  166. package/src/components/new-trace-viewer/components/copy-button.tsx +68 -0
  167. package/src/components/new-trace-viewer/components/event-list.tsx +109 -0
  168. package/src/components/new-trace-viewer/components/split-pane.tsx +186 -0
  169. package/src/components/new-trace-viewer/components/timeline.tsx +378 -0
  170. package/src/components/new-trace-viewer/context.tsx +94 -0
  171. package/src/components/new-trace-viewer/detail-panel.tsx +57 -0
  172. package/src/components/new-trace-viewer/icons.tsx +79 -0
  173. package/src/components/new-trace-viewer/trace-viewer.tsx +585 -0
  174. package/src/components/new-trace-viewer/types.ts +0 -0
  175. package/src/components/new-trace-viewer/util/constants.ts +0 -0
  176. package/src/components/new-trace-viewer/utils.ts +524 -0
  177. package/src/components/run-trace-view.tsx +3 -0
  178. package/src/components/sidebar/attribute-panel.tsx +200 -138
  179. package/src/components/sidebar/copyable-data-block.tsx +1 -1
  180. package/src/components/sidebar/detail-card.tsx +1 -19
  181. package/src/components/sidebar/entity-detail-panel.tsx +91 -147
  182. package/src/components/sidebar/events-list.tsx +9 -6
  183. package/src/components/sidebar/sidebar-data-context.tsx +62 -0
  184. package/src/components/stream-viewer.tsx +18 -40
  185. package/src/components/trace-viewer-new.tsx +49 -0
  186. package/src/components/ui/data-inspector.tsx +436 -31
  187. package/src/components/ui/inspector-theme.ts +39 -81
  188. package/src/components/workflow-trace-view.tsx +12 -2
  189. package/src/components/workflow-traces/trace-span-construction.ts +1 -1
  190. package/src/index.ts +2 -0
  191. package/src/lib/hydration.ts +37 -0
  192. package/src/lib/stream-display.ts +48 -0
  193. package/src/lib/utils.ts +18 -23
  194. package/src/styles.css +470 -1
@@ -9,7 +9,14 @@
9
9
  */
10
10
 
11
11
  import { Lock } from 'lucide-react';
12
- import { createContext, useContext, useEffect, useRef, useState } from 'react';
12
+ import {
13
+ createContext,
14
+ useContext,
15
+ useEffect,
16
+ useMemo,
17
+ useRef,
18
+ useState,
19
+ } from 'react';
13
20
  import {
14
21
  ObjectInspector,
15
22
  ObjectLabel,
@@ -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,7 @@ import {
26
38
  inspectorThemeExtendedLight,
27
39
  inspectorThemeLight,
28
40
  } from './inspector-theme';
41
+ import { Spinner } from './spinner';
29
42
 
30
43
  // ---------------------------------------------------------------------------
31
44
  // StreamRef / ClassInstanceRef type detection
@@ -34,19 +47,62 @@ import {
34
47
 
35
48
  const STREAM_REF_TYPE = '__workflow_stream_ref__';
36
49
  const CLASS_INSTANCE_REF_TYPE = '__workflow_class_instance_ref__';
50
+ const RUN_REF_TYPE = '__workflow_run_ref__';
51
+ const BYTES_DISPLAY_TYPE = '__workflow_bytes_display__';
37
52
 
38
53
  interface StreamRef {
39
54
  __type: typeof STREAM_REF_TYPE;
40
55
  streamId: string;
41
56
  }
42
57
 
58
+ interface RunRef {
59
+ __type: typeof RUN_REF_TYPE;
60
+ runId: string;
61
+ }
62
+
63
+ interface BytesDisplay {
64
+ __type: typeof BYTES_DISPLAY_TYPE;
65
+ text: string;
66
+ decodedFrom?: DecodedStreamChunkSource;
67
+ }
68
+
69
+ function deserializeChunkText(text: string): string {
70
+ try {
71
+ const parsed = JSON.parse(text);
72
+ if (typeof parsed === 'string') {
73
+ return parsed;
74
+ }
75
+ return JSON.stringify(parsed, null, 2);
76
+ } catch {
77
+ return text;
78
+ }
79
+ }
80
+
81
+ function parseChunkData(text: string): unknown {
82
+ try {
83
+ return JSON.parse(text);
84
+ } catch {
85
+ return text;
86
+ }
87
+ }
88
+
43
89
  function isStreamRef(value: unknown): value is StreamRef {
44
- return (
45
- value !== null &&
46
- typeof value === 'object' &&
47
- '__type' in value &&
48
- (value as Record<string, unknown>).__type === STREAM_REF_TYPE
49
- );
90
+ if (value === null || typeof value !== 'object') return false;
91
+ // Check both enumerable and non-enumerable __type (opaque refs use non-enumerable)
92
+ const desc = Object.getOwnPropertyDescriptor(value, '__type');
93
+ return desc?.value === STREAM_REF_TYPE;
94
+ }
95
+
96
+ function isRunRef(value: unknown): value is RunRef {
97
+ if (value === null || typeof value !== 'object') return false;
98
+ const desc = Object.getOwnPropertyDescriptor(value, '__type');
99
+ return desc?.value === RUN_REF_TYPE;
100
+ }
101
+
102
+ export function isBytesDisplay(value: unknown): value is BytesDisplay {
103
+ if (value === null || typeof value !== 'object') return false;
104
+ const desc = Object.getOwnPropertyDescriptor(value, '__type');
105
+ return desc?.value === BYTES_DISPLAY_TYPE;
50
106
  }
51
107
 
52
108
  function isClassInstanceRef(value: unknown): value is {
@@ -75,18 +131,89 @@ export const StreamClickContext = createContext<
75
131
  ((streamId: string) => void) | undefined
76
132
  >(undefined);
77
133
 
134
+ /**
135
+ * Context for passing a decrypt handler down to DataInspector instances.
136
+ * When provided, encrypted markers become clickable buttons that trigger decryption.
137
+ */
138
+ export type DecryptClickContextValue = {
139
+ onDecrypt: () => void;
140
+ isDecrypting: boolean;
141
+ };
142
+
143
+ export const DecryptClickContext = createContext<
144
+ DecryptClickContextValue | undefined
145
+ >(undefined);
146
+
147
+ export const RunClickContext = createContext<
148
+ ((runId: string) => void) | undefined
149
+ >(undefined);
150
+
151
+ function EncryptedInlineLabel() {
152
+ const ctx = useContext(DecryptClickContext);
153
+ if (ctx) {
154
+ return (
155
+ <button
156
+ type="button"
157
+ className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[11px] cursor-pointer"
158
+ style={{
159
+ backgroundColor: 'var(--ds-gray-100)',
160
+ color: 'var(--ds-gray-700)',
161
+ border: '1px solid var(--ds-gray-400)',
162
+ fontStyle: 'italic',
163
+ opacity: ctx.isDecrypting ? 0.6 : 1,
164
+ }}
165
+ disabled={ctx.isDecrypting}
166
+ onClick={(e) => {
167
+ e.stopPropagation();
168
+ ctx.onDecrypt();
169
+ }}
170
+ title="Click to decrypt"
171
+ >
172
+ {ctx.isDecrypting ? (
173
+ <Spinner size={12} />
174
+ ) : (
175
+ <Lock
176
+ className="h-3 w-3"
177
+ style={{ display: 'inline', flexShrink: 0 }}
178
+ />
179
+ )}
180
+ <span>{ctx.isDecrypting ? 'Decrypting…' : 'Decrypt'}</span>
181
+ </button>
182
+ );
183
+ }
184
+ return (
185
+ <span style={{ color: 'var(--ds-gray-600)', fontStyle: 'italic' }}>
186
+ <Lock
187
+ className="h-3 w-3"
188
+ style={{
189
+ display: 'inline',
190
+ verticalAlign: 'middle',
191
+ marginRight: '3px',
192
+ marginTop: '-1px',
193
+ }}
194
+ />
195
+ Encrypted
196
+ </span>
197
+ );
198
+ }
78
199
  function StreamRefInline({ streamRef }: { streamRef: StreamRef }) {
79
200
  const onStreamClick = useContext(StreamClickContext);
201
+ const [hovered, setHovered] = useState(false);
80
202
  return (
81
203
  <button
82
204
  type="button"
83
- className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-mono cursor-pointer"
205
+ 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
206
  style={{
85
- backgroundColor: 'var(--ds-blue-100)',
86
- color: 'var(--ds-blue-800)',
207
+ backgroundColor: hovered ? 'var(--ds-blue-200)' : 'var(--ds-blue-100)',
208
+ color: 'var(--ds-blue-900)',
87
209
  border: '1px solid var(--ds-blue-300)',
88
210
  }}
89
- onClick={() => onStreamClick?.(streamRef.streamId)}
211
+ onMouseEnter={() => setHovered(true)}
212
+ onMouseLeave={() => setHovered(false)}
213
+ onClick={(e) => {
214
+ e.stopPropagation();
215
+ onStreamClick?.(streamRef.streamId);
216
+ }}
90
217
  title={`View stream: ${streamRef.streamId}`}
91
218
  >
92
219
  <span>📡</span>
@@ -95,6 +222,33 @@ function StreamRefInline({ streamRef }: { streamRef: StreamRef }) {
95
222
  );
96
223
  }
97
224
 
225
+ function RunRefInline({ runRef }: { runRef: RunRef }) {
226
+ const onRunClick = useContext(RunClickContext);
227
+ const [hovered, setHovered] = useState(false);
228
+ return (
229
+ <button
230
+ type="button"
231
+ 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"
232
+ style={{
233
+ backgroundColor: hovered
234
+ ? 'var(--ds-purple-200)'
235
+ : 'var(--ds-purple-100)',
236
+ color: 'var(--ds-purple-900)',
237
+ border: '1px solid var(--ds-purple-300)',
238
+ }}
239
+ onMouseEnter={() => setHovered(true)}
240
+ onMouseLeave={() => setHovered(false)}
241
+ onClick={(e) => {
242
+ e.stopPropagation();
243
+ onRunClick?.(runRef.runId);
244
+ }}
245
+ title={`View run: ${runRef.runId}`}
246
+ >
247
+ <span>{runRef.runId}</span>
248
+ </button>
249
+ );
250
+ }
251
+
98
252
  // ---------------------------------------------------------------------------
99
253
  // Extended theme context (for colors react-inspector doesn't support natively)
100
254
  // ---------------------------------------------------------------------------
@@ -103,6 +257,147 @@ const ExtendedThemeContext = createContext<InspectorThemeExtended>(
103
257
  inspectorThemeExtendedLight
104
258
  );
105
259
 
260
+ function DecodedBytesChunk({
261
+ decodedText,
262
+ source,
263
+ }: {
264
+ decodedText: string;
265
+ source: DecodedStreamChunkSource;
266
+ }) {
267
+ const [selectedView, setSelectedView] = useState<'decoded' | 'bytes'>(
268
+ 'decoded'
269
+ );
270
+ const parsed = parseChunkData(decodedText);
271
+
272
+ return (
273
+ <div className="min-w-0">
274
+ {selectedView === 'decoded' ? (
275
+ <div className="min-w-0">
276
+ {typeof parsed === 'string' ? (
277
+ <span
278
+ className="whitespace-pre-wrap break-words"
279
+ style={{ color: 'var(--ds-gray-1000)' }}
280
+ >
281
+ {deserializeChunkText(parsed)}
282
+ </span>
283
+ ) : (
284
+ <DataInspector data={parsed} expandLevel={1} />
285
+ )}
286
+ </div>
287
+ ) : (
288
+ <DecodedBytesInspector decodedText={decodedText} source={source} />
289
+ )}
290
+ <div className="mt-2 flex">
291
+ <div
292
+ className="inline-flex overflow-hidden rounded border"
293
+ style={{ borderColor: 'var(--ds-gray-400)' }}
294
+ title={`${source.type} decoded as ${source.encoding.toUpperCase()} text. Switch to Bytes to inspect the summarized raw value.`}
295
+ >
296
+ <button
297
+ type="button"
298
+ className="h-5 px-1.5 text-[10px] font-medium"
299
+ style={{
300
+ backgroundColor:
301
+ selectedView === 'decoded'
302
+ ? 'var(--ds-gray-200)'
303
+ : 'var(--ds-gray-100)',
304
+ color: 'var(--ds-gray-900)',
305
+ }}
306
+ onClick={() => setSelectedView('decoded')}
307
+ aria-pressed={selectedView === 'decoded'}
308
+ aria-label="Show decoded text"
309
+ >
310
+ Decoded
311
+ </button>
312
+ <button
313
+ type="button"
314
+ className="h-5 border-l px-1.5 text-[10px] font-medium"
315
+ style={{
316
+ borderColor: 'var(--ds-gray-400)',
317
+ backgroundColor:
318
+ selectedView === 'bytes'
319
+ ? 'var(--ds-gray-200)'
320
+ : 'var(--ds-gray-100)',
321
+ color: 'var(--ds-gray-900)',
322
+ }}
323
+ onClick={() => setSelectedView('bytes')}
324
+ aria-pressed={selectedView === 'bytes'}
325
+ aria-label="Show raw bytes summary"
326
+ >
327
+ Bytes
328
+ </button>
329
+ </div>
330
+ </div>
331
+ </div>
332
+ );
333
+ }
334
+
335
+ function DecodedBytesInspector({
336
+ decodedText,
337
+ source,
338
+ }: {
339
+ decodedText: string;
340
+ source: DecodedStreamChunkSource;
341
+ }) {
342
+ const [expanded, setExpanded] = useState(false);
343
+
344
+ return (
345
+ <div className="font-mono">
346
+ <button
347
+ type="button"
348
+ className="flex max-w-full items-start gap-1 text-left"
349
+ style={{ color: 'var(--ds-gray-1000)' }}
350
+ onClick={() => setExpanded((value) => !value)}
351
+ title={`${source.type} decoded as ${source.encoding.toUpperCase()} text`}
352
+ >
353
+ <span className="select-none" style={{ color: 'var(--ds-gray-700)' }}>
354
+ {expanded ? '▼' : '▶'}
355
+ </span>
356
+ <span className="min-w-0 break-words">{source.rawSummary}</span>
357
+ </button>
358
+ {expanded && (
359
+ <div className="mt-1 pl-5">
360
+ <span style={{ color: 'var(--ds-gray-700)' }}>decoded: </span>
361
+ <span
362
+ className="whitespace-pre-wrap break-words"
363
+ style={{ color: 'var(--ds-green-900)' }}
364
+ >
365
+ {JSON.stringify(decodedText)}
366
+ </span>
367
+ </div>
368
+ )}
369
+ </div>
370
+ );
371
+ }
372
+
373
+ function BytesDisplayLabel({
374
+ name,
375
+ display,
376
+ }: {
377
+ name?: string;
378
+ display: BytesDisplay;
379
+ }) {
380
+ return (
381
+ <div className="inline-block min-w-0 align-top">
382
+ {name != null && <ObjectName name={name} />}
383
+ {name != null && <span>: </span>}
384
+ {display.decodedFrom ? (
385
+ <DecodedBytesChunk
386
+ decodedText={display.text}
387
+ source={display.decodedFrom}
388
+ />
389
+ ) : (
390
+ <span
391
+ className="whitespace-pre-wrap break-words"
392
+ style={{ color: 'var(--ds-gray-1000)' }}
393
+ >
394
+ {display.text}
395
+ </span>
396
+ )}
397
+ </div>
398
+ );
399
+ }
400
+
106
401
  // ---------------------------------------------------------------------------
107
402
  // Custom nodeRenderer
108
403
  // ---------------------------------------------------------------------------
@@ -132,26 +427,17 @@ function NodeRenderer({
132
427
  }) {
133
428
  const extendedTheme = useContext(ExtendedThemeContext);
134
429
 
135
- // Encrypted marker → flat label with Lock icon, non-expandable
430
+ if (isBytesDisplay(data)) {
431
+ return <BytesDisplayLabel name={name} display={data} />;
432
+ }
433
+
434
+ // Encrypted marker → flat label with Lock icon, clickable when onDecrypt is available
136
435
  if (
137
436
  data !== null &&
138
437
  typeof data === 'object' &&
139
438
  data.constructor?.name === ENCRYPTED_DISPLAY_NAME
140
439
  ) {
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
- );
440
+ const label = <EncryptedInlineLabel />;
155
441
  if (depth === 0) {
156
442
  return label;
157
443
  }
@@ -175,6 +461,17 @@ function NodeRenderer({
175
461
  );
176
462
  }
177
463
 
464
+ // RunRef → inline clickable badge linking to the target run
465
+ if (isRunRef(data)) {
466
+ return (
467
+ <span>
468
+ {name != null && <ObjectName name={name} />}
469
+ {name != null && <span>: </span>}
470
+ <RunRefInline runRef={data} />
471
+ </span>
472
+ );
473
+ }
474
+
178
475
  // ClassInstanceRef → show className as type, data as the inspectable value
179
476
  if (isClassInstanceRef(data)) {
180
477
  if (depth === 0) {
@@ -225,6 +522,76 @@ function NodeRenderer({
225
522
  // Public component
226
523
  // ---------------------------------------------------------------------------
227
524
 
525
+ /**
526
+ * Create a non-expandable wrapper that carries ref data as non-enumerable
527
+ * properties. ObjectInspector won't render children for objects with no
528
+ * enumerable keys, but our NodeRenderer can still detect them.
529
+ */
530
+ function makeOpaqueRef(ref: Record<string, unknown>): unknown {
531
+ const opaque = Object.create(null);
532
+ for (const [key, value] of Object.entries(ref)) {
533
+ Object.defineProperty(opaque, key, { value, enumerable: false });
534
+ }
535
+ return opaque;
536
+ }
537
+
538
+ function makeBytesDisplay(display: FormattedStreamChunkDisplay): unknown {
539
+ const opaque = Object.create(null);
540
+ Object.defineProperty(opaque, '__type', {
541
+ value: BYTES_DISPLAY_TYPE,
542
+ enumerable: false,
543
+ });
544
+ Object.defineProperty(opaque, 'text', {
545
+ value: display.text,
546
+ enumerable: false,
547
+ });
548
+ Object.defineProperty(opaque, 'decodedFrom', {
549
+ value: display.decodedFrom,
550
+ enumerable: false,
551
+ });
552
+ return opaque;
553
+ }
554
+
555
+ /**
556
+ * Recursively walk data and replace RunRef/StreamRef/typed array objects with
557
+ * non-expandable versions so ObjectInspector doesn't show their internals.
558
+ * Only recurses into plain objects and arrays to avoid stripping class
559
+ * instances (Date, Error, URL, Headers, etc.) that have their own rendering in
560
+ * NodeRenderer. Map and Set containers are preserved while their contents are
561
+ * prepared for display.
562
+ *
563
+ * Exported for testing the typed-array detection path used by hydrated
564
+ * AI agent stream chunks (e.g. `{ delta: new Uint8Array(...) }`).
565
+ */
566
+ export function collapseRefs(data: unknown): unknown {
567
+ if (data === null || typeof data !== 'object') return data;
568
+ if (ArrayBuffer.isView(data) && !(data instanceof DataView)) {
569
+ return makeBytesDisplay(formatArrayBufferViewForDisplay(data));
570
+ }
571
+ if (isRunRef(data) || isStreamRef(data))
572
+ return makeOpaqueRef(data as unknown as Record<string, unknown>);
573
+ if (Array.isArray(data)) return data.map(collapseRefs);
574
+ if (data instanceof Map) {
575
+ return new Map(
576
+ Array.from(data.entries(), ([key, value]) => [
577
+ collapseRefs(key),
578
+ collapseRefs(value),
579
+ ])
580
+ );
581
+ }
582
+ if (data instanceof Set) {
583
+ return new Set(Array.from(data.values(), collapseRefs));
584
+ }
585
+ // Only recurse into plain objects — leave class instances untouched
586
+ const proto = Object.getPrototypeOf(data);
587
+ if (proto !== Object.prototype && proto !== null) return data;
588
+ const result: Record<string, unknown> = {};
589
+ for (const [key, value] of Object.entries(data)) {
590
+ result[key] = collapseRefs(value);
591
+ }
592
+ return result;
593
+ }
594
+
228
595
  export interface DataInspectorProps {
229
596
  /** The data to inspect */
230
597
  data: unknown;
@@ -234,6 +601,12 @@ export interface DataInspectorProps {
234
601
  name?: string;
235
602
  /** Callback when a stream reference is clicked */
236
603
  onStreamClick?: (streamId: string) => void;
604
+ /** Callback when a run reference is clicked */
605
+ onRunClick?: (runId: string) => void;
606
+ /** Callback when an encrypted marker is clicked (triggers decryption) */
607
+ onDecrypt?: () => void;
608
+ /** Whether decryption is currently in progress */
609
+ isDecrypting?: boolean;
237
610
  }
238
611
 
239
612
  export function DataInspector({
@@ -241,8 +614,12 @@ export function DataInspector({
241
614
  expandLevel = 2,
242
615
  name,
243
616
  onStreamClick,
617
+ onRunClick,
618
+ onDecrypt,
619
+ isDecrypting = false,
244
620
  }: DataInspectorProps) {
245
- const stableData = useStableInspectorData(data);
621
+ const collapsedData = useMemo(() => collapseRefs(data), [data]);
622
+ const stableData = useStableInspectorData(collapsedData);
246
623
  const [initialExpandLevel, setInitialExpandLevel] = useState(expandLevel);
247
624
  const isDark = useDarkMode();
248
625
  const extendedTheme = isDark
@@ -269,16 +646,31 @@ export function DataInspector({
269
646
  </ExtendedThemeContext.Provider>
270
647
  );
271
648
 
272
- // Wrap in StreamClickContext if a handler is provided
649
+ let wrapped = content;
650
+
273
651
  if (onStreamClick) {
274
- return (
652
+ wrapped = (
275
653
  <StreamClickContext.Provider value={onStreamClick}>
276
- {content}
654
+ {wrapped}
277
655
  </StreamClickContext.Provider>
278
656
  );
279
657
  }
658
+ if (onRunClick) {
659
+ wrapped = (
660
+ <RunClickContext.Provider value={onRunClick}>
661
+ {wrapped}
662
+ </RunClickContext.Provider>
663
+ );
664
+ }
665
+ if (onDecrypt) {
666
+ wrapped = (
667
+ <DecryptClickContext.Provider value={{ onDecrypt, isDecrypting }}>
668
+ {wrapped}
669
+ </DecryptClickContext.Provider>
670
+ );
671
+ }
280
672
 
281
- return content;
673
+ return wrapped;
282
674
  }
283
675
 
284
676
  function useStableInspectorData<T>(next: T): T {
@@ -293,9 +685,22 @@ function isObjectLike(value: unknown): value is Record<string, unknown> {
293
685
  return typeof value === 'object' && value !== null;
294
686
  }
295
687
 
688
+ function isSameBytesDisplay(a: BytesDisplay, b: BytesDisplay): boolean {
689
+ return (
690
+ a.text === b.text &&
691
+ a.decodedFrom?.type === b.decodedFrom?.type &&
692
+ a.decodedFrom?.encoding === b.decodedFrom?.encoding &&
693
+ a.decodedFrom?.rawSummary === b.decodedFrom?.rawSummary
694
+ );
695
+ }
696
+
296
697
  function isDeepEqual(a: unknown, b: unknown, seen = new WeakMap()): boolean {
297
698
  if (Object.is(a, b)) return true;
298
699
 
700
+ if (isBytesDisplay(a) || isBytesDisplay(b)) {
701
+ return isBytesDisplay(a) && isBytesDisplay(b) && isSameBytesDisplay(a, b);
702
+ }
703
+
299
704
  if (a instanceof Date && b instanceof Date) {
300
705
  return a.getTime() === b.getTime();
301
706
  }