@workflow/web-shared 5.0.0-beta.2 → 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 (127) hide show
  1. package/dist/components/index.d.ts +2 -0
  2. package/dist/components/index.d.ts.map +1 -1
  3. package/dist/components/index.js +2 -0
  4. package/dist/components/index.js.map +1 -1
  5. package/dist/components/new-trace-viewer/components/copy-button.d.ts +9 -0
  6. package/dist/components/new-trace-viewer/components/copy-button.d.ts.map +1 -0
  7. package/dist/components/new-trace-viewer/components/copy-button.js +27 -0
  8. package/dist/components/new-trace-viewer/components/copy-button.js.map +1 -0
  9. package/dist/components/new-trace-viewer/components/event-list.d.ts +8 -0
  10. package/dist/components/new-trace-viewer/components/event-list.d.ts.map +1 -0
  11. package/dist/components/new-trace-viewer/components/event-list.js +35 -0
  12. package/dist/components/new-trace-viewer/components/event-list.js.map +1 -0
  13. package/dist/components/new-trace-viewer/components/split-pane.d.ts +14 -0
  14. package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -0
  15. package/dist/components/new-trace-viewer/components/split-pane.js +100 -0
  16. package/dist/components/new-trace-viewer/components/split-pane.js.map +1 -0
  17. package/dist/components/new-trace-viewer/components/timeline.d.ts +32 -0
  18. package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -0
  19. package/dist/components/new-trace-viewer/components/timeline.js +134 -0
  20. package/dist/components/new-trace-viewer/components/timeline.js.map +1 -0
  21. package/dist/components/new-trace-viewer/context.d.ts +15 -0
  22. package/dist/components/new-trace-viewer/context.d.ts.map +1 -0
  23. package/dist/components/new-trace-viewer/context.js +49 -0
  24. package/dist/components/new-trace-viewer/context.js.map +1 -0
  25. package/dist/components/new-trace-viewer/detail-panel.d.ts +10 -0
  26. package/dist/components/new-trace-viewer/detail-panel.d.ts.map +1 -0
  27. package/dist/components/new-trace-viewer/detail-panel.js +11 -0
  28. package/dist/components/new-trace-viewer/detail-panel.js.map +1 -0
  29. package/dist/components/new-trace-viewer/icons.d.ts +6 -0
  30. package/dist/components/new-trace-viewer/icons.d.ts.map +1 -0
  31. package/dist/components/new-trace-viewer/icons.js +15 -0
  32. package/dist/components/new-trace-viewer/icons.js.map +1 -0
  33. package/dist/components/new-trace-viewer/trace-viewer.d.ts +8 -0
  34. package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -0
  35. package/dist/components/new-trace-viewer/trace-viewer.js +329 -0
  36. package/dist/components/new-trace-viewer/trace-viewer.js.map +1 -0
  37. package/dist/components/new-trace-viewer/types.d.ts +1 -0
  38. package/dist/components/new-trace-viewer/types.d.ts.map +1 -0
  39. package/dist/components/new-trace-viewer/types.js +2 -0
  40. package/dist/components/new-trace-viewer/types.js.map +1 -0
  41. package/dist/components/new-trace-viewer/util/constants.d.ts +1 -0
  42. package/dist/components/new-trace-viewer/util/constants.d.ts.map +1 -0
  43. package/dist/components/new-trace-viewer/util/constants.js +2 -0
  44. package/dist/components/new-trace-viewer/util/constants.js.map +1 -0
  45. package/dist/components/new-trace-viewer/utils.d.ts +47 -0
  46. package/dist/components/new-trace-viewer/utils.d.ts.map +1 -0
  47. package/dist/components/new-trace-viewer/utils.js +358 -0
  48. package/dist/components/new-trace-viewer/utils.js.map +1 -0
  49. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  50. package/dist/components/sidebar/attribute-panel.js +42 -23
  51. package/dist/components/sidebar/attribute-panel.js.map +1 -1
  52. package/dist/components/sidebar/copyable-data-block.js +1 -1
  53. package/dist/components/sidebar/copyable-data-block.js.map +1 -1
  54. package/dist/components/sidebar/detail-card.d.ts.map +1 -1
  55. package/dist/components/sidebar/detail-card.js +1 -1
  56. package/dist/components/sidebar/detail-card.js.map +1 -1
  57. package/dist/components/sidebar/entity-detail-panel.d.ts +1 -3
  58. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  59. package/dist/components/sidebar/entity-detail-panel.js +3 -25
  60. package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
  61. package/dist/components/sidebar/events-list.d.ts.map +1 -1
  62. package/dist/components/sidebar/events-list.js +2 -2
  63. package/dist/components/sidebar/events-list.js.map +1 -1
  64. package/dist/components/sidebar/sidebar-data-context.d.ts +29 -0
  65. package/dist/components/sidebar/sidebar-data-context.d.ts.map +1 -0
  66. package/dist/components/sidebar/sidebar-data-context.js +19 -0
  67. package/dist/components/sidebar/sidebar-data-context.js.map +1 -0
  68. package/dist/components/stream-viewer.d.ts +1 -1
  69. package/dist/components/stream-viewer.d.ts.map +1 -1
  70. package/dist/components/stream-viewer.js +2 -26
  71. package/dist/components/stream-viewer.js.map +1 -1
  72. package/dist/components/trace-viewer-new.d.ts +10 -0
  73. package/dist/components/trace-viewer-new.d.ts.map +1 -0
  74. package/dist/components/trace-viewer-new.js +23 -0
  75. package/dist/components/trace-viewer-new.js.map +1 -0
  76. package/dist/components/ui/data-inspector.d.ts +21 -0
  77. package/dist/components/ui/data-inspector.d.ts.map +1 -1
  78. package/dist/components/ui/data-inspector.js +99 -4
  79. package/dist/components/ui/data-inspector.js.map +1 -1
  80. package/dist/components/ui/inspector-theme.d.ts +9 -10
  81. package/dist/components/ui/inspector-theme.d.ts.map +1 -1
  82. package/dist/components/ui/inspector-theme.js +38 -71
  83. package/dist/components/ui/inspector-theme.js.map +1 -1
  84. package/dist/components/workflow-trace-view.d.ts +1 -3
  85. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  86. package/dist/components/workflow-trace-view.js +4 -4
  87. package/dist/components/workflow-trace-view.js.map +1 -1
  88. package/dist/index.d.ts +1 -0
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js.map +1 -1
  91. package/dist/lib/stream-display.d.ts +18 -0
  92. package/dist/lib/stream-display.d.ts.map +1 -0
  93. package/dist/lib/stream-display.js +30 -0
  94. package/dist/lib/stream-display.js.map +1 -0
  95. package/dist/lib/utils.d.ts +7 -9
  96. package/dist/lib/utils.d.ts.map +1 -1
  97. package/dist/lib/utils.js +17 -22
  98. package/dist/lib/utils.js.map +1 -1
  99. package/dist/styles.css +470 -1
  100. package/package.json +3 -2
  101. package/src/components/index.ts +5 -0
  102. package/src/components/new-trace-viewer/components/copy-button.tsx +68 -0
  103. package/src/components/new-trace-viewer/components/event-list.tsx +109 -0
  104. package/src/components/new-trace-viewer/components/split-pane.tsx +186 -0
  105. package/src/components/new-trace-viewer/components/timeline.tsx +378 -0
  106. package/src/components/new-trace-viewer/context.tsx +94 -0
  107. package/src/components/new-trace-viewer/detail-panel.tsx +57 -0
  108. package/src/components/new-trace-viewer/icons.tsx +79 -0
  109. package/src/components/new-trace-viewer/trace-viewer.tsx +585 -0
  110. package/src/components/new-trace-viewer/types.ts +0 -0
  111. package/src/components/new-trace-viewer/util/constants.ts +0 -0
  112. package/src/components/new-trace-viewer/utils.ts +524 -0
  113. package/src/components/sidebar/attribute-panel.tsx +43 -32
  114. package/src/components/sidebar/copyable-data-block.tsx +1 -1
  115. package/src/components/sidebar/detail-card.tsx +1 -19
  116. package/src/components/sidebar/entity-detail-panel.tsx +0 -71
  117. package/src/components/sidebar/events-list.tsx +9 -6
  118. package/src/components/sidebar/sidebar-data-context.tsx +62 -0
  119. package/src/components/stream-viewer.tsx +18 -40
  120. package/src/components/trace-viewer-new.tsx +49 -0
  121. package/src/components/ui/data-inspector.tsx +235 -4
  122. package/src/components/ui/inspector-theme.ts +39 -81
  123. package/src/components/workflow-trace-view.tsx +2 -6
  124. package/src/index.ts +1 -0
  125. package/src/lib/stream-display.ts +48 -0
  126. package/src/lib/utils.ts +18 -23
  127. package/src/styles.css +470 -1
@@ -59,25 +59,7 @@ export function DetailCard({
59
59
  {summary}
60
60
  </span>
61
61
  </summary>
62
- {/* Expanded content with connecting line */}
63
- <div className={`relative pl-6 mt-3 ${contentClassName ?? ''}`}>
64
- {/* Curved connecting line - vertical part from summary */}
65
- <div
66
- className="absolute left-3 -top-3 w-px h-3"
67
- style={{ backgroundColor: 'var(--ds-gray-400)' }}
68
- />
69
- {/* Curved corner */}
70
- <div
71
- className="absolute left-3 top-0 w-3 h-3 border-l border-b rounded-bl-lg"
72
- style={{ borderColor: 'var(--ds-gray-400)' }}
73
- />
74
- {/* Horizontal part to content */}
75
- <div
76
- className="absolute left-6 top-3 w-0 h-px -translate-y-px"
77
- style={{ backgroundColor: 'var(--ds-gray-400)' }}
78
- />
79
- <div>{children}</div>
80
- </div>
62
+ <div className={`mt-2 ${contentClassName ?? ''}`}>{children}</div>
81
63
  </details>
82
64
  );
83
65
  }
@@ -4,10 +4,8 @@ import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
4
4
  import clsx from 'clsx';
5
5
  import { Send, Zap } from 'lucide-react';
6
6
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
7
- import { isEncryptedMarker } from '../../lib/hydration';
8
7
  import { useToast } from '../../lib/toast';
9
8
  import { DecryptClickContext } from '../ui/data-inspector';
10
- import { DecryptButton } from '../ui/decrypt-button';
11
9
  import { AttributePanel } from './attribute-panel';
12
10
  import { EventsList } from './events-list';
13
11
  import { ResolveHookModal } from './resolve-hook-modal';
@@ -69,7 +67,6 @@ export function EntityDetailPanel({
69
67
  onDecrypt,
70
68
  isDecrypting = false,
71
69
  selectedSpan,
72
- hasEncryptedData = false,
73
70
  }: {
74
71
  run: WorkflowRun;
75
72
  /** Callback when a stream reference is clicked */
@@ -108,8 +105,6 @@ export function EntityDetailPanel({
108
105
  isDecrypting?: boolean;
109
106
  /** Info about the currently selected span from the trace viewer */
110
107
  selectedSpan: SelectedSpanInfo | null;
111
- /** Run-level hint: the run contains encrypted data (from probe). */
112
- hasEncryptedData?: boolean;
113
108
  }): React.JSX.Element | null {
114
109
  const toast = useToast();
115
110
  const [stoppingSleep, setStoppingSleep] = useState(false);
@@ -216,17 +211,6 @@ export function EntityDetailPanel({
216
211
  const error = spanDetailError ?? undefined;
217
212
  const loading = spanDetailLoading ?? false;
218
213
 
219
- const hasEncryptedFields = useMemo(() => {
220
- if (!spanDetailData) return false;
221
- const d = spanDetailData as Record<string, unknown>;
222
- return (
223
- isEncryptedMarker(d.input) ||
224
- isEncryptedMarker(d.output) ||
225
- isEncryptedMarker(d.error) ||
226
- isEncryptedMarker(d.metadata)
227
- );
228
- }, [spanDetailData]);
229
-
230
214
  // Get the hook token for resolving (prefer fetched data, then hooks array fallback)
231
215
  const hookToken = useMemo(() => {
232
216
  if (resource !== 'hook' || !resourceId) return undefined;
@@ -354,61 +338,12 @@ export function EntityDetailPanel({
354
338
  return null;
355
339
  }
356
340
 
357
- const resourceLabel = resource.charAt(0).toUpperCase() + resource.slice(1);
358
341
  const hasPendingActions =
359
342
  (resource === 'sleep' && canWakeUp) ||
360
343
  (resource === 'hook' && canResolveHook);
361
- const runStateLabel = run.completedAt ? 'Completed' : 'Live';
362
344
 
363
345
  return (
364
346
  <div className="flex h-full flex-col">
365
- <div
366
- className="border-b px-3 py-3"
367
- style={{ borderColor: 'var(--ds-gray-200)' }}
368
- >
369
- <div className="flex items-start justify-between gap-2">
370
- <div className="min-w-0">
371
- <div className="flex items-center gap-2">
372
- <span
373
- className="inline-flex items-center rounded-full border px-2 py-0.5 text-[13px] font-medium"
374
- style={{
375
- borderColor: 'var(--ds-gray-300)',
376
- color: 'var(--ds-gray-900)',
377
- backgroundColor: 'var(--ds-background-100)',
378
- }}
379
- >
380
- {resourceLabel}
381
- </span>
382
- <span
383
- className="text-[13px]"
384
- style={{
385
- color: run.completedAt
386
- ? 'var(--ds-gray-700)'
387
- : 'var(--ds-green-800)',
388
- }}
389
- >
390
- {runStateLabel}
391
- </span>
392
- </div>
393
- <p
394
- className="mt-1 truncate font-mono text-[13px]"
395
- style={{ color: 'var(--ds-gray-700)' }}
396
- title={resourceId}
397
- >
398
- {resourceId}
399
- </p>
400
- </div>
401
- {(hasEncryptedFields || hasEncryptedData || encryptionKey) &&
402
- onDecrypt && (
403
- <DecryptButton
404
- decrypted={!!encryptionKey}
405
- loading={isDecrypting}
406
- onClick={onDecrypt}
407
- />
408
- )}
409
- </div>
410
- </div>
411
-
412
347
  <DecryptClickContext.Provider
413
348
  value={onDecrypt ? { onDecrypt, isDecrypting } : undefined}
414
349
  >
@@ -479,12 +414,6 @@ export function EntityDetailPanel({
479
414
 
480
415
  <div className="space-y-4">
481
416
  <section>
482
- <h3
483
- className="mb-2 text-[13px] font-medium uppercase tracking-wide"
484
- style={{ color: 'var(--ds-gray-700)' }}
485
- >
486
- Details
487
- </h3>
488
417
  <AttributePanel
489
418
  data={displayData}
490
419
  moduleSpecifier={moduleSpecifier}
@@ -142,8 +142,11 @@ function EventItem({
142
142
  className="flex min-h-[32px] items-center justify-between gap-4 px-2.5 py-1.5"
143
143
  style={{ borderColor: 'var(--ds-gray-300)' }}
144
144
  >
145
- <span className="text-[14px]" style={{ color: 'var(--ds-gray-700)' }}>
146
- eventId
145
+ <span
146
+ className="text-[13px] font-medium"
147
+ style={{ color: 'var(--ds-gray-700)' }}
148
+ >
149
+ Event ID
147
150
  </span>
148
151
  <span
149
152
  className="max-w-[70%] truncate text-right text-[13px] font-mono"
@@ -159,10 +162,10 @@ function EventItem({
159
162
  style={{ borderColor: 'var(--ds-gray-300)' }}
160
163
  >
161
164
  <span
162
- className="text-[14px]"
165
+ className="text-[13px] font-medium"
163
166
  style={{ color: 'var(--ds-gray-700)' }}
164
167
  >
165
- correlationId
168
+ Correlation ID
166
169
  </span>
167
170
  <span
168
171
  className="max-w-[70%] truncate text-right text-[13px] font-mono"
@@ -311,10 +314,10 @@ export function EventsList({
311
314
  return (
312
315
  <div className="mt-2" style={{ color: 'var(--ds-gray-1000)' }}>
313
316
  <h3
314
- className="text-heading-16 font-medium mt-4 mb-2"
317
+ className="text-label-14 font-medium mt-4 mb-2"
315
318
  style={{ color: 'var(--ds-gray-1000)' }}
316
319
  >
317
- Events {!isLoading && `(${sortedEvents.length})`}
320
+ Events
318
321
  </h3>
319
322
  {isLoading ? (
320
323
  <div className="flex flex-col gap-4">
@@ -0,0 +1,62 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext, type ReactNode } from 'react';
4
+ import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
5
+ import type { SpanSelectionInfo } from './entity-detail-panel';
6
+
7
+ export interface SidebarDataContextValue {
8
+ run: WorkflowRun;
9
+ events: Event[];
10
+ spanDetailData: WorkflowRun | Step | Hook | Event | null;
11
+ spanDetailError?: Error | null;
12
+ spanDetailLoading?: boolean;
13
+ onSpanSelect: (info: SpanSelectionInfo) => void;
14
+ onStreamClick?: (streamId: string) => void;
15
+ onRunClick?: (runId: string) => void;
16
+ onWakeUpSleep?: (
17
+ runId: string,
18
+ correlationId: string
19
+ ) => Promise<{ stoppedCount: number }>;
20
+ onLoadEventData?: (
21
+ correlationId: string,
22
+ eventId: string
23
+ ) => Promise<unknown | null>;
24
+ onResolveHook?: (
25
+ hookToken: string,
26
+ payload: unknown,
27
+ hook?: Hook
28
+ ) => Promise<void>;
29
+ encryptionKey?: Uint8Array;
30
+ onDecrypt?: () => void;
31
+ isDecrypting?: boolean;
32
+ hasEncryptedData?: boolean;
33
+ }
34
+
35
+ const SidebarDataContext = createContext<SidebarDataContextValue | null>(null);
36
+ SidebarDataContext.displayName = 'SidebarDataContext';
37
+
38
+ export function SidebarDataProvider({
39
+ value,
40
+ children,
41
+ }: {
42
+ value: SidebarDataContextValue;
43
+ children: ReactNode;
44
+ }) {
45
+ return (
46
+ <SidebarDataContext.Provider value={value}>
47
+ {children}
48
+ </SidebarDataContext.Provider>
49
+ );
50
+ }
51
+
52
+ export function useSidebarData(): SidebarDataContextValue {
53
+ const ctx = useContext(SidebarDataContext);
54
+ if (!ctx) {
55
+ throw new Error('useSidebarData must be used within a SidebarDataProvider');
56
+ }
57
+ return ctx;
58
+ }
59
+
60
+ export function useSidebarDataOptional(): SidebarDataContextValue | null {
61
+ return useContext(SidebarDataContext);
62
+ }
@@ -5,37 +5,13 @@ import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso';
5
5
  import { DataInspector } from './ui/data-inspector';
6
6
  import { Skeleton } from './ui/skeleton';
7
7
 
8
- // ──────────────────────────────────────────────────────────────────────────
9
- // Helpers
10
- // ──────────────────────────────────────────────────────────────────────────
11
-
12
- function deserializeChunkText(text: string): string {
13
- try {
14
- const parsed = JSON.parse(text);
15
- if (typeof parsed === 'string') {
16
- return parsed;
17
- }
18
- return JSON.stringify(parsed, null, 2);
19
- } catch {
20
- return text;
21
- }
22
- }
23
-
24
- function parseChunkData(text: string): unknown {
25
- try {
26
- return JSON.parse(text);
27
- } catch {
28
- return text;
29
- }
30
- }
31
-
32
8
  // ──────────────────────────────────────────────────────────────────────────
33
9
  // Types
34
10
  // ──────────────────────────────────────────────────────────────────────────
35
11
 
36
12
  export interface StreamChunk {
37
13
  id: number;
38
- text: string;
14
+ value: unknown;
39
15
  }
40
16
 
41
17
  type Chunk = StreamChunk;
@@ -62,8 +38,6 @@ const ChunkRow = React.memo(function ChunkRow({
62
38
  chunk: Chunk;
63
39
  index: number;
64
40
  }) {
65
- const parsed = parseChunkData(chunk.text);
66
-
67
41
  return (
68
42
  <div
69
43
  className="text-[11px] rounded-md border p-3"
@@ -72,22 +46,26 @@ const ChunkRow = React.memo(function ChunkRow({
72
46
  backgroundColor: 'var(--ds-gray-100)',
73
47
  }}
74
48
  >
75
- <span
76
- className="select-none mr-2"
77
- style={{ color: 'var(--ds-gray-500)' }}
78
- >
79
- [{index}]
80
- </span>
81
- {typeof parsed === 'string' ? (
49
+ <div className="flex items-start gap-2">
82
50
  <span
83
- className="whitespace-pre-wrap break-words"
84
- style={{ color: 'var(--ds-gray-1000)' }}
51
+ className="select-none pt-px"
52
+ style={{ color: 'var(--ds-gray-500)' }}
85
53
  >
86
- {deserializeChunkText(parsed)}
54
+ [{index}]
87
55
  </span>
88
- ) : (
89
- <DataInspector data={parsed} expandLevel={1} />
90
- )}
56
+ <div className="min-w-0 flex-1">
57
+ {typeof chunk.value === 'string' ? (
58
+ <span
59
+ className="whitespace-pre-wrap break-words"
60
+ style={{ color: 'var(--ds-gray-1000)' }}
61
+ >
62
+ {chunk.value}
63
+ </span>
64
+ ) : (
65
+ <DataInspector data={chunk.value} expandLevel={1} />
66
+ )}
67
+ </div>
68
+ </div>
91
69
  </div>
92
70
  );
93
71
  });
@@ -0,0 +1,49 @@
1
+ import { useMemo } from 'react';
2
+ import type { WorkflowRun } from '@workflow/core/runtime';
3
+ import type { Event } from '@workflow/world';
4
+ import { buildTrace, type TraceWithMeta } from '../lib/trace-builder';
5
+ import { NewTraceViewer as NewTraceViewerComponent } from './new-trace-viewer/trace-viewer';
6
+ import {
7
+ SidebarDataProvider,
8
+ type SidebarDataContextValue,
9
+ } from './sidebar/sidebar-data-context';
10
+ import type { Trace } from './trace-viewer/types';
11
+
12
+ const NewTraceViewer = ({
13
+ run,
14
+ events,
15
+ sidebarData,
16
+ }: {
17
+ run: WorkflowRun;
18
+ events: Event[];
19
+ sidebarData: SidebarDataContextValue;
20
+ }) => {
21
+ // Build trace only when actual data changes — no timer-driven rebuilds.
22
+ // Active span widths are animated imperatively by useLiveTick at 60fps.
23
+ const traceWithMeta: TraceWithMeta | undefined = useMemo(() => {
24
+ if (!run?.runId) {
25
+ return undefined;
26
+ }
27
+ return buildTrace(run, events, new Date());
28
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- `new Date()` is intentionally not a dep; useLiveTick handles live growth
29
+ }, [run, events]);
30
+ const trace = traceWithMeta;
31
+
32
+ if (!trace) {
33
+ return (
34
+ <div className="relative w-full h-full flex items-center justify-center">
35
+ <div className="text-gray-500 text-sm">Loading trace…</div>
36
+ </div>
37
+ );
38
+ }
39
+
40
+ return (
41
+ <SidebarDataProvider value={sidebarData}>
42
+ <div className="relative w-full h-full flex">
43
+ <NewTraceViewerComponent trace={trace as Trace} />
44
+ </div>
45
+ </SidebarDataProvider>
46
+ );
47
+ };
48
+
49
+ export { NewTraceViewer };
@@ -26,6 +26,11 @@ import {
26
26
  } from 'react-inspector';
27
27
  import { useDarkMode } from '../../hooks/use-dark-mode';
28
28
  import { ENCRYPTED_DISPLAY_NAME } from '../../lib/hydration';
29
+ import {
30
+ type DecodedStreamChunkSource,
31
+ type FormattedStreamChunkDisplay,
32
+ formatArrayBufferViewForDisplay,
33
+ } from '../../lib/stream-display';
29
34
  import {
30
35
  type InspectorThemeExtended,
31
36
  inspectorThemeDark,
@@ -43,6 +48,7 @@ import { Spinner } from './spinner';
43
48
  const STREAM_REF_TYPE = '__workflow_stream_ref__';
44
49
  const CLASS_INSTANCE_REF_TYPE = '__workflow_class_instance_ref__';
45
50
  const RUN_REF_TYPE = '__workflow_run_ref__';
51
+ const BYTES_DISPLAY_TYPE = '__workflow_bytes_display__';
46
52
 
47
53
  interface StreamRef {
48
54
  __type: typeof STREAM_REF_TYPE;
@@ -54,6 +60,32 @@ interface RunRef {
54
60
  runId: string;
55
61
  }
56
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
+
57
89
  function isStreamRef(value: unknown): value is StreamRef {
58
90
  if (value === null || typeof value !== 'object') return false;
59
91
  // Check both enumerable and non-enumerable __type (opaque refs use non-enumerable)
@@ -67,6 +99,12 @@ function isRunRef(value: unknown): value is RunRef {
67
99
  return desc?.value === RUN_REF_TYPE;
68
100
  }
69
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;
106
+ }
107
+
70
108
  function isClassInstanceRef(value: unknown): value is {
71
109
  __type: string;
72
110
  className: string;
@@ -219,6 +257,147 @@ const ExtendedThemeContext = createContext<InspectorThemeExtended>(
219
257
  inspectorThemeExtendedLight
220
258
  );
221
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
+
222
401
  // ---------------------------------------------------------------------------
223
402
  // Custom nodeRenderer
224
403
  // ---------------------------------------------------------------------------
@@ -248,6 +427,10 @@ function NodeRenderer({
248
427
  }) {
249
428
  const extendedTheme = useContext(ExtendedThemeContext);
250
429
 
430
+ if (isBytesDisplay(data)) {
431
+ return <BytesDisplayLabel name={name} display={data} />;
432
+ }
433
+
251
434
  // Encrypted marker → flat label with Lock icon, clickable when onDecrypt is available
252
435
  if (
253
436
  data !== null &&
@@ -352,18 +535,53 @@ function makeOpaqueRef(ref: Record<string, unknown>): unknown {
352
535
  return opaque;
353
536
  }
354
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
+
355
555
  /**
356
- * Recursively walk data and replace RunRef/StreamRef objects with
556
+ * Recursively walk data and replace RunRef/StreamRef/typed array objects with
357
557
  * non-expandable versions so ObjectInspector doesn't show their internals.
358
558
  * Only recurses into plain objects and arrays to avoid stripping class
359
- * instances (Date, Error, Map, Set, URL, Headers, etc.) that have their
360
- * own rendering in NodeRenderer.
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(...) }`).
361
565
  */
362
- function collapseRefs(data: unknown): unknown {
566
+ export function collapseRefs(data: unknown): unknown {
363
567
  if (data === null || typeof data !== 'object') return data;
568
+ if (ArrayBuffer.isView(data) && !(data instanceof DataView)) {
569
+ return makeBytesDisplay(formatArrayBufferViewForDisplay(data));
570
+ }
364
571
  if (isRunRef(data) || isStreamRef(data))
365
572
  return makeOpaqueRef(data as unknown as Record<string, unknown>);
366
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
+ }
367
585
  // Only recurse into plain objects — leave class instances untouched
368
586
  const proto = Object.getPrototypeOf(data);
369
587
  if (proto !== Object.prototype && proto !== null) return data;
@@ -467,9 +685,22 @@ function isObjectLike(value: unknown): value is Record<string, unknown> {
467
685
  return typeof value === 'object' && value !== null;
468
686
  }
469
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
+
470
697
  function isDeepEqual(a: unknown, b: unknown, seen = new WeakMap()): boolean {
471
698
  if (Object.is(a, b)) return true;
472
699
 
700
+ if (isBytesDisplay(a) || isBytesDisplay(b)) {
701
+ return isBytesDisplay(a) && isBytesDisplay(b) && isSameBytesDisplay(a, b);
702
+ }
703
+
473
704
  if (a instanceof Date && b instanceof Date) {
474
705
  return a.getTime() === b.getTime();
475
706
  }