@workflow/web-shared 4.1.0-beta.65 → 4.1.0-beta.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +52 -16
- package/dist/components/event-list-view.js.map +1 -1
- package/dist/components/hook-actions.d.ts.map +1 -1
- package/dist/components/hook-actions.js +2 -1
- package/dist/components/hook-actions.js.map +1 -1
- package/dist/components/sidebar/attribute-panel.d.ts +3 -1
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +77 -40
- package/dist/components/sidebar/attribute-panel.js.map +1 -1
- package/dist/components/sidebar/copyable-data-block.d.ts.map +1 -1
- package/dist/components/sidebar/copyable-data-block.js +2 -1
- package/dist/components/sidebar/copyable-data-block.js.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +12 -6
- package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +33 -5
- package/dist/components/sidebar/events-list.js.map +1 -1
- package/dist/components/trace-viewer/components/span-segments.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/span-segments.js +54 -1
- package/dist/components/trace-viewer/components/span-segments.js.map +1 -1
- package/dist/components/trace-viewer/util/timing.d.ts.map +1 -1
- package/dist/components/trace-viewer/util/timing.js +1 -2
- package/dist/components/trace-viewer/util/timing.js.map +1 -1
- package/dist/components/ui/error-stack-block.d.ts.map +1 -1
- package/dist/components/ui/error-stack-block.js +2 -1
- package/dist/components/ui/error-stack-block.js.map +1 -1
- package/dist/components/ui/timestamp-tooltip.d.ts +6 -0
- package/dist/components/ui/timestamp-tooltip.d.ts.map +1 -0
- package/dist/components/ui/timestamp-tooltip.js +200 -0
- package/dist/components/ui/timestamp-tooltip.js.map +1 -0
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +8 -1
- package/dist/components/workflow-trace-view.js.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +10 -7
- package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/hydration.d.ts +3 -0
- package/dist/lib/hydration.d.ts.map +1 -1
- package/dist/lib/hydration.js +36 -10
- package/dist/lib/hydration.js.map +1 -1
- package/dist/lib/toast.d.ts +25 -0
- package/dist/lib/toast.d.ts.map +1 -0
- package/dist/lib/toast.js +24 -0
- package/dist/lib/toast.js.map +1 -0
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +7 -3
- package/dist/lib/utils.js.map +1 -1
- package/package.json +6 -4
- package/src/components/event-list-view.tsx +125 -35
- package/src/components/hook-actions.tsx +2 -1
- package/src/components/sidebar/attribute-panel.tsx +85 -35
- package/src/components/sidebar/copyable-data-block.tsx +2 -1
- package/src/components/sidebar/entity-detail-panel.tsx +13 -5
- package/src/components/sidebar/events-list.tsx +51 -13
- package/src/components/trace-viewer/components/span-segments.ts +70 -1
- package/src/components/trace-viewer/util/timing.ts +1 -2
- package/src/components/ui/error-stack-block.tsx +2 -1
- package/src/components/ui/timestamp-tooltip.tsx +326 -0
- package/src/components/workflow-trace-view.tsx +8 -1
- package/src/components/workflow-traces/trace-span-construction.ts +12 -7
- package/src/index.ts +2 -0
- package/src/lib/hydration.ts +43 -9
- package/src/lib/toast.tsx +42 -0
- package/src/lib/utils.ts +7 -3
|
@@ -6,8 +6,8 @@ import type { ModelMessage } from 'ai';
|
|
|
6
6
|
import { Lock } from 'lucide-react';
|
|
7
7
|
import type { KeyboardEvent, ReactNode } from 'react';
|
|
8
8
|
import { useCallback, useMemo, useState } from 'react';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { isEncryptedMarker, isExpiredMarker } from '../../lib/hydration';
|
|
10
|
+
import { useToast } from '../../lib/toast';
|
|
11
11
|
import { extractConversation, isDoStreamStep } from '../../lib/utils';
|
|
12
12
|
import { StreamClickContext } from '../ui/data-inspector';
|
|
13
13
|
import { ErrorCard } from '../ui/error-card';
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
isStructuredErrorWithStack,
|
|
17
17
|
} from '../ui/error-stack-block';
|
|
18
18
|
import { Skeleton } from '../ui/skeleton';
|
|
19
|
+
import { TimestampTooltip } from '../ui/timestamp-tooltip';
|
|
19
20
|
import { ConversationView } from './conversation-view';
|
|
20
21
|
import { CopyableDataBlock } from './copyable-data-block';
|
|
21
22
|
import { DetailCard } from './detail-card';
|
|
@@ -194,6 +195,24 @@ function EncryptedFieldBlock() {
|
|
|
194
195
|
);
|
|
195
196
|
}
|
|
196
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Inline display for an expired field — flat label indicating data is no longer available.
|
|
200
|
+
*/
|
|
201
|
+
function ExpiredFieldBlock() {
|
|
202
|
+
return (
|
|
203
|
+
<div
|
|
204
|
+
className="flex items-center gap-1.5 rounded-md border px-3 py-2 text-xs"
|
|
205
|
+
style={{
|
|
206
|
+
borderColor: 'var(--ds-gray-300)',
|
|
207
|
+
backgroundColor: 'var(--ds-gray-100)',
|
|
208
|
+
color: 'var(--ds-gray-700)',
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
<span className="font-medium">Data expired</span>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
197
216
|
function JsonBlock(value: unknown) {
|
|
198
217
|
return <CopyableDataBlock data={value} />;
|
|
199
218
|
}
|
|
@@ -336,6 +355,16 @@ const localMillisecondTimeOrNull = (value: unknown): string | null => {
|
|
|
336
355
|
return formatLocalMillisecondTime(date);
|
|
337
356
|
};
|
|
338
357
|
|
|
358
|
+
const timestampWithTooltipOrNull = (value: unknown): ReactNode | null => {
|
|
359
|
+
const date = parseDateValue(value);
|
|
360
|
+
if (!date) return null;
|
|
361
|
+
return (
|
|
362
|
+
<TimestampTooltip date={date}>
|
|
363
|
+
<span>{formatLocalMillisecondTime(date)}</span>
|
|
364
|
+
</TimestampTooltip>
|
|
365
|
+
);
|
|
366
|
+
};
|
|
367
|
+
|
|
339
368
|
interface DisplayContext {
|
|
340
369
|
stepName?: string;
|
|
341
370
|
}
|
|
@@ -375,23 +404,24 @@ const attributeToDisplayFn: Record<
|
|
|
375
404
|
projectId: (_value: unknown) => null,
|
|
376
405
|
environment: (_value: unknown) => null,
|
|
377
406
|
executionContext: (_value: unknown) => null,
|
|
378
|
-
// Dates
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
resumeAt: localMillisecondTimeOrNull,
|
|
407
|
+
// Dates — wrapped with TimestampTooltip showing UTC/local + relative time
|
|
408
|
+
createdAt: timestampWithTooltipOrNull,
|
|
409
|
+
startedAt: timestampWithTooltipOrNull,
|
|
410
|
+
updatedAt: timestampWithTooltipOrNull,
|
|
411
|
+
completedAt: timestampWithTooltipOrNull,
|
|
412
|
+
expiredAt: timestampWithTooltipOrNull,
|
|
413
|
+
retryAfter: timestampWithTooltipOrNull,
|
|
414
|
+
resumeAt: timestampWithTooltipOrNull,
|
|
387
415
|
// Resolved attributes, won't actually use this function
|
|
388
416
|
metadata: (value: unknown) => {
|
|
389
417
|
if (!hasDisplayContent(value)) return null;
|
|
390
418
|
if (isEncryptedMarker(value)) return <EncryptedFieldBlock />;
|
|
419
|
+
if (isExpiredMarker(value)) return <ExpiredFieldBlock />;
|
|
391
420
|
return JsonBlock(value);
|
|
392
421
|
},
|
|
393
422
|
input: (value: unknown, context?: DisplayContext) => {
|
|
394
423
|
if (isEncryptedMarker(value)) return <EncryptedFieldBlock />;
|
|
424
|
+
if (isExpiredMarker(value)) return <ExpiredFieldBlock />;
|
|
395
425
|
// Check if input has args + closure vars structure
|
|
396
426
|
if (value && typeof value === 'object' && 'args' in value) {
|
|
397
427
|
const { args, closureVars, thisVal } = value as {
|
|
@@ -496,6 +526,7 @@ const attributeToDisplayFn: Record<
|
|
|
496
526
|
output: (value: unknown) => {
|
|
497
527
|
if (!hasDisplayContent(value)) return null;
|
|
498
528
|
if (isEncryptedMarker(value)) return <EncryptedFieldBlock />;
|
|
529
|
+
if (isExpiredMarker(value)) return <ExpiredFieldBlock />;
|
|
499
530
|
return (
|
|
500
531
|
<DetailCard
|
|
501
532
|
summary="Output"
|
|
@@ -508,6 +539,7 @@ const attributeToDisplayFn: Record<
|
|
|
508
539
|
},
|
|
509
540
|
error: (value: unknown) => {
|
|
510
541
|
if (isEncryptedMarker(value)) return <EncryptedFieldBlock />;
|
|
542
|
+
if (isExpiredMarker(value)) return <ExpiredFieldBlock />;
|
|
511
543
|
if (!hasDisplayContent(value)) return null;
|
|
512
544
|
|
|
513
545
|
// If the error object has a `stack` field, render it as readable
|
|
@@ -536,6 +568,7 @@ const attributeToDisplayFn: Record<
|
|
|
536
568
|
},
|
|
537
569
|
eventData: (value: unknown) => {
|
|
538
570
|
if (isEncryptedMarker(value)) return <EncryptedFieldBlock />;
|
|
571
|
+
if (isExpiredMarker(value)) return <ExpiredFieldBlock />;
|
|
539
572
|
if (!hasDisplayContent(value)) return null;
|
|
540
573
|
return <DetailCard summary="Event Data">{JsonBlock(value)}</DetailCard>;
|
|
541
574
|
},
|
|
@@ -579,13 +612,7 @@ export const AttributeBlock = ({
|
|
|
579
612
|
attribute === 'input' ||
|
|
580
613
|
attribute === 'output' ||
|
|
581
614
|
attribute === 'eventData';
|
|
582
|
-
if (isLoading && isExpandableLoadingTarget) {
|
|
583
|
-
const label =
|
|
584
|
-
attribute === 'eventData'
|
|
585
|
-
? 'Event Data'
|
|
586
|
-
: attribute === 'output'
|
|
587
|
-
? 'Output'
|
|
588
|
-
: 'Input';
|
|
615
|
+
if (isLoading && isExpandableLoadingTarget && !hasDisplayContent(value)) {
|
|
589
616
|
return (
|
|
590
617
|
<div
|
|
591
618
|
className={`my-2 flex flex-col ${attribute === 'input' || attribute === 'output' ? 'gap-2 my-3.5' : 'gap-0'}`}
|
|
@@ -596,19 +623,7 @@ export const AttributeBlock = ({
|
|
|
596
623
|
>
|
|
597
624
|
{attribute}
|
|
598
625
|
</span>
|
|
599
|
-
<
|
|
600
|
-
summary={label}
|
|
601
|
-
summaryClassName="text-base py-2"
|
|
602
|
-
disabled
|
|
603
|
-
/>
|
|
604
|
-
<div
|
|
605
|
-
className="overflow-x-auto rounded-md border p-3"
|
|
606
|
-
style={{ borderColor: 'var(--ds-gray-300)' }}
|
|
607
|
-
>
|
|
608
|
-
<Skeleton className="h-4 w-[38%]" />
|
|
609
|
-
<Skeleton className="mt-2 h-4 w-[88%]" />
|
|
610
|
-
<Skeleton className="mt-2 h-4 w-[72%]" />
|
|
611
|
-
</div>
|
|
626
|
+
<Skeleton className="h-9 w-full rounded-md" />
|
|
612
627
|
</div>
|
|
613
628
|
);
|
|
614
629
|
}
|
|
@@ -674,6 +689,7 @@ export const AttributePanel = ({
|
|
|
674
689
|
error,
|
|
675
690
|
expiredAt,
|
|
676
691
|
onStreamClick,
|
|
692
|
+
resource,
|
|
677
693
|
}: {
|
|
678
694
|
data: Record<string, unknown>;
|
|
679
695
|
moduleSpecifier?: string;
|
|
@@ -682,7 +698,10 @@ export const AttributePanel = ({
|
|
|
682
698
|
expiredAt?: string | Date;
|
|
683
699
|
/** Callback when a stream reference is clicked */
|
|
684
700
|
onStreamClick?: (streamId: string) => void;
|
|
701
|
+
/** Resource type of the selected span — used to show targeted loading skeletons. */
|
|
702
|
+
resource?: string;
|
|
685
703
|
}) => {
|
|
704
|
+
const toast = useToast();
|
|
686
705
|
// Extract workflowCoreVersion from executionContext for display
|
|
687
706
|
const displayData = useMemo(() => {
|
|
688
707
|
const result = { ...data };
|
|
@@ -705,9 +724,23 @@ export const AttributePanel = ({
|
|
|
705
724
|
const basicAttributes = Object.keys(displayData)
|
|
706
725
|
.filter((key) => !resolvableAttributes.includes(key))
|
|
707
726
|
.sort(sortByAttributeOrder);
|
|
708
|
-
const resolvedAttributes =
|
|
709
|
-
|
|
710
|
-
|
|
727
|
+
const resolvedAttributes = useMemo(() => {
|
|
728
|
+
const present = Object.keys(displayData)
|
|
729
|
+
.filter((key) => resolvableAttributes.includes(key))
|
|
730
|
+
.sort(sortByAttributeOrder);
|
|
731
|
+
|
|
732
|
+
if (!isLoading) return present;
|
|
733
|
+
|
|
734
|
+
// During loading, ensure input/output appear so their skeletons render
|
|
735
|
+
// in the correct position (above the events section).
|
|
736
|
+
const loadingDefaults = ['input', 'output'];
|
|
737
|
+
for (const key of loadingDefaults) {
|
|
738
|
+
if (!present.includes(key)) {
|
|
739
|
+
present.push(key);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return present.sort(sortByAttributeOrder);
|
|
743
|
+
}, [displayData, isLoading]);
|
|
711
744
|
|
|
712
745
|
// Filter out attributes that return null
|
|
713
746
|
const visibleBasicAttributes = basicAttributes.filter((attribute) => {
|
|
@@ -784,7 +817,11 @@ export const AttributePanel = ({
|
|
|
784
817
|
? displayValue
|
|
785
818
|
: String(displayValue ?? displayData.moduleSpecifier ?? '');
|
|
786
819
|
const shouldCapitalizeLabel = attribute !== 'workflowCoreVersion';
|
|
787
|
-
const
|
|
820
|
+
const showResumeAtSkeleton =
|
|
821
|
+
isLoading && resource === 'sleep' && !displayData.resumeAt;
|
|
822
|
+
const showDivider =
|
|
823
|
+
index < orderedBasicAttributes.length - 1 ||
|
|
824
|
+
showResumeAtSkeleton;
|
|
788
825
|
|
|
789
826
|
return (
|
|
790
827
|
<div key={attribute} className="py-1">
|
|
@@ -834,6 +871,19 @@ export const AttributePanel = ({
|
|
|
834
871
|
</div>
|
|
835
872
|
);
|
|
836
873
|
})}
|
|
874
|
+
{isLoading && resource === 'sleep' && !displayData.resumeAt && (
|
|
875
|
+
<div className="py-1">
|
|
876
|
+
<div className="flex min-h-[32px] items-center justify-between gap-4 rounded-sm px-2.5 py-1">
|
|
877
|
+
<span
|
|
878
|
+
className="text-[14px] first-letter:uppercase"
|
|
879
|
+
style={{ color: 'var(--ds-gray-700)' }}
|
|
880
|
+
>
|
|
881
|
+
resumeAt
|
|
882
|
+
</span>
|
|
883
|
+
<Skeleton className="h-4 w-[55%]" />
|
|
884
|
+
</div>
|
|
885
|
+
</div>
|
|
886
|
+
)}
|
|
837
887
|
</div>
|
|
838
888
|
)}
|
|
839
889
|
{error ? (
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Copy } from 'lucide-react';
|
|
4
|
-
import {
|
|
4
|
+
import { useToast } from '../../lib/toast';
|
|
5
5
|
import { DataInspector } from '../ui/data-inspector';
|
|
6
6
|
|
|
7
7
|
const serializeForClipboard = (value: unknown): string => {
|
|
@@ -21,6 +21,7 @@ const serializeForClipboard = (value: unknown): string => {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export function CopyableDataBlock({ data }: { data: unknown }) {
|
|
24
|
+
const toast = useToast();
|
|
24
25
|
return (
|
|
25
26
|
<div
|
|
26
27
|
className="relative overflow-x-auto rounded-md border p-3 pt-9"
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
5
|
import { Send, Zap } from 'lucide-react';
|
|
6
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
7
|
-
import {
|
|
6
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
7
|
+
import { useToast } from '../../lib/toast';
|
|
8
8
|
import { isEncryptedMarker } from '../../lib/hydration';
|
|
9
9
|
import { DecryptButton } from '../ui/decrypt-button';
|
|
10
10
|
import { AttributePanel } from './attribute-panel';
|
|
@@ -104,6 +104,7 @@ export function EntityDetailPanel({
|
|
|
104
104
|
/** Info about the currently selected span from the trace viewer */
|
|
105
105
|
selectedSpan: SelectedSpanInfo | null;
|
|
106
106
|
}): React.JSX.Element | null {
|
|
107
|
+
const toast = useToast();
|
|
107
108
|
const [stoppingSleep, setStoppingSleep] = useState(false);
|
|
108
109
|
const [showResolveHookModal, setShowResolveHookModal] = useState(false);
|
|
109
110
|
const [resolvingHook, setResolvingHook] = useState(false);
|
|
@@ -144,20 +145,26 @@ export function EntityDetailPanel({
|
|
|
144
145
|
return { resource: undefined, resourceId: undefined, runId: undefined };
|
|
145
146
|
}, [selectedSpan, data]);
|
|
146
147
|
|
|
147
|
-
// Notify parent when span selection changes
|
|
148
|
+
// Notify parent when span selection changes.
|
|
149
|
+
// Use a ref for the callback so the effect only fires when the actual
|
|
150
|
+
// selection values change, not when the callback identity changes due to
|
|
151
|
+
// parent re-renders from polling.
|
|
152
|
+
const onSpanSelectRef = useRef(onSpanSelect);
|
|
153
|
+
onSpanSelectRef.current = onSpanSelect;
|
|
154
|
+
|
|
148
155
|
useEffect(() => {
|
|
149
156
|
if (
|
|
150
157
|
resource &&
|
|
151
158
|
resourceId &&
|
|
152
159
|
['run', 'step', 'hook', 'sleep'].includes(resource)
|
|
153
160
|
) {
|
|
154
|
-
|
|
161
|
+
onSpanSelectRef.current({
|
|
155
162
|
resource: resource as 'run' | 'step' | 'hook' | 'sleep',
|
|
156
163
|
resourceId,
|
|
157
164
|
runId,
|
|
158
165
|
});
|
|
159
166
|
}
|
|
160
|
-
}, [resource, resourceId, runId
|
|
167
|
+
}, [resource, resourceId, runId]);
|
|
161
168
|
|
|
162
169
|
// Check if this sleep is still pending and can be woken up
|
|
163
170
|
const canWakeUp = useMemo(() => {
|
|
@@ -474,6 +481,7 @@ export function EntityDetailPanel({
|
|
|
474
481
|
isLoading={loading}
|
|
475
482
|
error={error ?? undefined}
|
|
476
483
|
onStreamClick={onStreamClick}
|
|
484
|
+
resource={resource}
|
|
477
485
|
/>
|
|
478
486
|
</section>
|
|
479
487
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import type { Event } from '@workflow/world';
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import { isExpiredMarker } from '../../lib/hydration';
|
|
6
|
+
import { ErrorCard } from '../ui/error-card';
|
|
5
7
|
import {
|
|
6
8
|
ErrorStackBlock,
|
|
7
9
|
isStructuredErrorWithStack,
|
|
@@ -23,10 +25,14 @@ const DATA_EVENT_TYPES = new Set([
|
|
|
23
25
|
'step_created',
|
|
24
26
|
'step_completed',
|
|
25
27
|
'step_failed',
|
|
28
|
+
'step_retrying',
|
|
26
29
|
'hook_created',
|
|
27
30
|
'hook_received',
|
|
28
31
|
'run_created',
|
|
29
32
|
'run_completed',
|
|
33
|
+
'run_failed',
|
|
34
|
+
'wait_created',
|
|
35
|
+
'wait_completed',
|
|
30
36
|
]);
|
|
31
37
|
|
|
32
38
|
/**
|
|
@@ -175,15 +181,11 @@ function EventItem({
|
|
|
175
181
|
|
|
176
182
|
{/* Error state */}
|
|
177
183
|
{loadError && (
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}}
|
|
184
|
-
>
|
|
185
|
-
{loadError}
|
|
186
|
-
</div>
|
|
184
|
+
<ErrorCard
|
|
185
|
+
title="Failed to load event data"
|
|
186
|
+
details={loadError}
|
|
187
|
+
className="mt-2"
|
|
188
|
+
/>
|
|
187
189
|
)}
|
|
188
190
|
|
|
189
191
|
{/* Event data */}
|
|
@@ -196,6 +198,24 @@ function EventItem({
|
|
|
196
198
|
);
|
|
197
199
|
}
|
|
198
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Check if an eventData object has only expired marker values in its serialized
|
|
203
|
+
* sub-fields (result, input, output, metadata, payload). Non-serialized fields
|
|
204
|
+
* like `resumeAt` or `reason` are ignored.
|
|
205
|
+
*/
|
|
206
|
+
function hasOnlyExpiredFields(data: unknown): boolean {
|
|
207
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
const record = data as Record<string, unknown>;
|
|
211
|
+
const serializedKeys = ['result', 'input', 'output', 'metadata', 'payload'];
|
|
212
|
+
const presentKeys = serializedKeys.filter((k) => k in record);
|
|
213
|
+
return (
|
|
214
|
+
presentKeys.length > 0 &&
|
|
215
|
+
presentKeys.every((k) => isExpiredMarker(record[k]))
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
199
219
|
/**
|
|
200
220
|
* Renders event data, using ErrorStackBlock for error events that contain
|
|
201
221
|
* a structured error with a stack trace, and CopyableDataBlock otherwise.
|
|
@@ -207,6 +227,24 @@ function EventDataBlock({
|
|
|
207
227
|
eventType: string;
|
|
208
228
|
data: unknown;
|
|
209
229
|
}) {
|
|
230
|
+
// Expired data — show a simple message instead of the raw stub.
|
|
231
|
+
// Check both the top-level eventData and nested sub-fields (result, input, etc.)
|
|
232
|
+
// since the server stubs each ref field independently.
|
|
233
|
+
if (isExpiredMarker(data) || hasOnlyExpiredFields(data)) {
|
|
234
|
+
return (
|
|
235
|
+
<div
|
|
236
|
+
className="flex items-center gap-1.5 rounded-md border px-3 py-2 text-xs"
|
|
237
|
+
style={{
|
|
238
|
+
borderColor: 'var(--ds-gray-300)',
|
|
239
|
+
backgroundColor: 'var(--ds-gray-100)',
|
|
240
|
+
color: 'var(--ds-gray-700)',
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
<span className="font-medium">Data expired</span>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
210
248
|
// For error events (step_failed, step_retrying), the eventData has the shape
|
|
211
249
|
// { error: StructuredError, stack?: string, ... }. Check both the top-level
|
|
212
250
|
// value and the nested `error` field for a stack trace.
|
|
@@ -269,10 +307,10 @@ export function EventsList({
|
|
|
269
307
|
Events {!isLoading && `(${sortedEvents.length})`}
|
|
270
308
|
</h3>
|
|
271
309
|
{isLoading ? (
|
|
272
|
-
<div className="flex flex-col gap-
|
|
273
|
-
<Skeleton className="h-
|
|
274
|
-
<Skeleton className="h-
|
|
275
|
-
<Skeleton className="h-
|
|
310
|
+
<div className="flex flex-col gap-4">
|
|
311
|
+
<Skeleton className="h-9 w-full rounded-md" />
|
|
312
|
+
<Skeleton className="h-9 w-full rounded-md" />
|
|
313
|
+
<Skeleton className="h-9 w-full rounded-md" />
|
|
276
314
|
</div>
|
|
277
315
|
) : null}
|
|
278
316
|
{!isLoading && !error && sortedEvents.length === 0 && (
|
|
@@ -341,6 +341,26 @@ function computeRunSegments(node: SpanNode): Segment[] {
|
|
|
341
341
|
|
|
342
342
|
if (duration <= 0) return segments;
|
|
343
343
|
|
|
344
|
+
// V1 runs (specVersion 1) don't emit run lifecycle events (run_created,
|
|
345
|
+
// run_started, run_completed). Detect this by checking for the absence of
|
|
346
|
+
// run_created — it's always the first event for v2 runs and is always
|
|
347
|
+
// loaded in the first page, so its absence reliably signals a v1 run.
|
|
348
|
+
// For v1, fall back to the run entity's status from span.attributes.data.
|
|
349
|
+
const hasRunCreated = events.some((e) => e.event.name === 'run_created');
|
|
350
|
+
|
|
351
|
+
if (!hasRunCreated) {
|
|
352
|
+
const runData = node.span.attributes?.data as
|
|
353
|
+
| Record<string, unknown>
|
|
354
|
+
| undefined;
|
|
355
|
+
const runStatus = runData?.status as string | undefined;
|
|
356
|
+
return computeV1RunSegments(
|
|
357
|
+
startTime,
|
|
358
|
+
duration,
|
|
359
|
+
activeStartTime,
|
|
360
|
+
runStatus
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
344
364
|
const failedEvent = events.find((e) => e.event.name === 'run_failed');
|
|
345
365
|
const completedEvent = events.find((e) => e.event.name === 'run_completed');
|
|
346
366
|
|
|
@@ -396,7 +416,56 @@ function computeRunSegments(node: SpanNode): Segment[] {
|
|
|
396
416
|
status: 'succeeded',
|
|
397
417
|
});
|
|
398
418
|
} else {
|
|
399
|
-
// Running to completion
|
|
419
|
+
// Running to completion (or terminal events haven't loaded yet)
|
|
420
|
+
segments.push({
|
|
421
|
+
startFraction: cursor,
|
|
422
|
+
endFraction: 1,
|
|
423
|
+
status: 'running',
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return segments;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Compute segments for a v1 run using only the run entity's status and
|
|
432
|
+
* activeStartTime (derived from run.startedAt). V1 runs have no run
|
|
433
|
+
* lifecycle events, so we infer the visual segments from entity fields.
|
|
434
|
+
*/
|
|
435
|
+
function computeV1RunSegments(
|
|
436
|
+
startTime: number,
|
|
437
|
+
duration: number,
|
|
438
|
+
activeStartTime: number | undefined,
|
|
439
|
+
runStatus: string | undefined
|
|
440
|
+
): Segment[] {
|
|
441
|
+
const segments: Segment[] = [];
|
|
442
|
+
|
|
443
|
+
let cursor = 0;
|
|
444
|
+
if (activeStartTime && activeStartTime > startTime) {
|
|
445
|
+
const queuedFraction = timeToFraction(activeStartTime, startTime, duration);
|
|
446
|
+
if (queuedFraction > 0.001) {
|
|
447
|
+
segments.push({
|
|
448
|
+
startFraction: 0,
|
|
449
|
+
endFraction: queuedFraction,
|
|
450
|
+
status: 'queued',
|
|
451
|
+
});
|
|
452
|
+
cursor = queuedFraction;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (runStatus === 'failed') {
|
|
457
|
+
segments.push({
|
|
458
|
+
startFraction: cursor,
|
|
459
|
+
endFraction: 1,
|
|
460
|
+
status: 'failed',
|
|
461
|
+
});
|
|
462
|
+
} else if (runStatus === 'completed' || runStatus === 'cancelled') {
|
|
463
|
+
segments.push({
|
|
464
|
+
startFraction: cursor,
|
|
465
|
+
endFraction: 1,
|
|
466
|
+
status: 'succeeded',
|
|
467
|
+
});
|
|
468
|
+
} else {
|
|
400
469
|
segments.push({
|
|
401
470
|
startFraction: cursor,
|
|
402
471
|
endFraction: 1,
|
|
@@ -41,6 +41,5 @@ export function formatWallClockTime(epochMs: number): string {
|
|
|
41
41
|
const h = String(d.getHours()).padStart(2, '0');
|
|
42
42
|
const m = String(d.getMinutes()).padStart(2, '0');
|
|
43
43
|
const s = String(d.getSeconds()).padStart(2, '0');
|
|
44
|
-
|
|
45
|
-
return `${h}:${m}:${s}.${ms}`;
|
|
44
|
+
return `${h}:${m}:${s}`;
|
|
46
45
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { AlertCircle, Copy } from 'lucide-react';
|
|
4
|
-
import {
|
|
4
|
+
import { useToast } from '../../lib/toast';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Check whether `value` looks like a structured error object with a `stack`
|
|
@@ -28,6 +28,7 @@ export function ErrorStackBlock({
|
|
|
28
28
|
}: {
|
|
29
29
|
value: Record<string, unknown> & { stack: string };
|
|
30
30
|
}) {
|
|
31
|
+
const toast = useToast();
|
|
31
32
|
const stack = value.stack;
|
|
32
33
|
const message = typeof value.message === 'string' ? value.message : undefined;
|
|
33
34
|
const copyText = message ? `${message}\n\n${stack}` : stack;
|