@workflow/web-shared 5.0.0-beta.14 → 5.0.0-beta.16
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 +1 -1
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +20 -3
- package/dist/components/new-trace-viewer/components/timeline.d.ts +1 -1
- package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/timeline.js +61 -13
- package/dist/components/new-trace-viewer/components/timeline.module.css +31 -0
- package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/trace-viewer.js +8 -9
- package/dist/components/new-trace-viewer/utils.d.ts +1 -1
- package/dist/components/new-trace-viewer/utils.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/utils.js +12 -5
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +26 -32
- package/dist/components/sidebar/attributes-block.d.ts +28 -0
- package/dist/components/sidebar/attributes-block.d.ts.map +1 -0
- package/dist/components/sidebar/attributes-block.js +83 -0
- package/dist/components/sidebar/entity-detail-panel.js +2 -2
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +8 -1
- package/dist/components/trace-viewer/components/node.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/node.js +2 -1
- package/dist/components/ui/context-card.d.ts +47 -0
- package/dist/components/ui/context-card.d.ts.map +1 -0
- package/dist/components/ui/context-card.js +471 -0
- package/dist/components/ui/timestamp-tooltip.d.ts +34 -1
- package/dist/components/ui/timestamp-tooltip.d.ts.map +1 -1
- package/dist/components/ui/timestamp-tooltip.js +80 -132
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +8 -10
- package/dist/components/workflow-traces/event-colors.d.ts +1 -0
- package/dist/components/workflow-traces/event-colors.d.ts.map +1 -1
- package/dist/components/workflow-traces/event-colors.js +12 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +2 -1
- package/dist/lib/trace-builder.d.ts +15 -0
- package/dist/lib/trace-builder.d.ts.map +1 -1
- package/dist/lib/trace-builder.js +27 -2
- package/dist/styles.css +9 -0
- package/package.json +6 -4
- package/src/components/event-list-view.tsx +25 -2
- package/src/components/new-trace-viewer/components/timeline.module.css +31 -0
- package/src/components/new-trace-viewer/components/timeline.tsx +134 -10
- package/src/components/new-trace-viewer/trace-viewer.tsx +17 -14
- package/src/components/new-trace-viewer/utils.ts +12 -4
- package/src/components/sidebar/attribute-panel.tsx +117 -119
- package/src/components/sidebar/attributes-block.tsx +182 -0
- package/src/components/sidebar/entity-detail-panel.tsx +1 -1
- package/src/components/sidebar/events-list.tsx +8 -0
- package/src/components/trace-viewer/components/node.tsx +3 -2
- package/src/components/ui/context-card.tsx +784 -0
- package/src/components/ui/timestamp-tooltip.tsx +132 -194
- package/src/components/workflow-trace-view.tsx +18 -10
- package/src/components/workflow-traces/event-colors.ts +12 -0
- package/src/components/workflow-traces/trace-span-construction.ts +1 -0
- package/src/lib/trace-builder.ts +32 -1
- package/src/styles.css +9 -0
|
@@ -4,13 +4,14 @@ import { ArrowLeft, ArrowRight } from 'lucide-react';
|
|
|
4
4
|
import type { CSSProperties, ReactNode } from 'react';
|
|
5
5
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
6
|
import { cn } from '../../../lib/utils';
|
|
7
|
-
import type { Span } from '../types';
|
|
8
7
|
import {
|
|
9
8
|
formatDuration,
|
|
10
9
|
formatDurationPrecise,
|
|
11
10
|
getHighResInMs,
|
|
12
11
|
} from '../../trace-viewer/util/timing';
|
|
12
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip';
|
|
13
13
|
import { isSpanDimmedBySearch, type SpanSearchResult } from '../search';
|
|
14
|
+
import type { Span } from '../types';
|
|
14
15
|
import type { Segment, SegmentStatus, TimeMarker } from '../utils';
|
|
15
16
|
import {
|
|
16
17
|
computeSpanGaps,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
getResourceColor,
|
|
19
20
|
getSpanDurationMs,
|
|
20
21
|
} from '../utils';
|
|
22
|
+
import styles from './timeline.module.css';
|
|
21
23
|
import { ROW_HEIGHT_PX, useRowWindow } from './use-row-window';
|
|
22
24
|
|
|
23
25
|
// ---------------------------------------------------------------------------
|
|
@@ -33,6 +35,7 @@ const SEGMENT_CLASSES: Record<SegmentStatus, string> = {
|
|
|
33
35
|
retrying: 'bg-gray-400 border border-gray-500',
|
|
34
36
|
waiting: 'bg-gray-400 border border-gray-500',
|
|
35
37
|
running: 'bg-blue-200 border border-blue-500',
|
|
38
|
+
completed: 'bg-blue-200 border border-blue-500',
|
|
36
39
|
failed: 'bg-red-200 border border-red-500',
|
|
37
40
|
succeeded: 'bg-green-200 border border-green-500',
|
|
38
41
|
sleeping: 'bg-gray-400 border border-gray-500',
|
|
@@ -44,6 +47,15 @@ const TIMELINE_INSET_STYLE: CSSProperties = {
|
|
|
44
47
|
right: TIMELINE_PADDING_PX,
|
|
45
48
|
};
|
|
46
49
|
|
|
50
|
+
const ACTIVE_SEGMENT_STATUSES: ReadonlySet<SegmentStatus> = new Set([
|
|
51
|
+
'running',
|
|
52
|
+
'received',
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
function RunningStripes(): ReactNode {
|
|
56
|
+
return <div aria-hidden className={styles.runningStripes} />;
|
|
57
|
+
}
|
|
58
|
+
|
|
47
59
|
// ---------------------------------------------------------------------------
|
|
48
60
|
// Bar geometry
|
|
49
61
|
// ---------------------------------------------------------------------------
|
|
@@ -222,25 +234,57 @@ function PlainBar({
|
|
|
222
234
|
);
|
|
223
235
|
}
|
|
224
236
|
|
|
237
|
+
function LeadInConnector({
|
|
238
|
+
leftPct,
|
|
239
|
+
widthPct,
|
|
240
|
+
label,
|
|
241
|
+
}: {
|
|
242
|
+
leftPct: number;
|
|
243
|
+
widthPct: number;
|
|
244
|
+
label: string | null;
|
|
245
|
+
}): ReactNode {
|
|
246
|
+
return (
|
|
247
|
+
<div
|
|
248
|
+
className="absolute top-1/2 h-6 -translate-y-1/2"
|
|
249
|
+
style={{
|
|
250
|
+
left: `calc(${leftPct}% + 0.5px)`,
|
|
251
|
+
width: `calc(${widthPct}% - 1px)`,
|
|
252
|
+
}}
|
|
253
|
+
>
|
|
254
|
+
<div className="absolute left-0 top-1/2 h-4 w-px -translate-y-1/2 bg-gray-500" />
|
|
255
|
+
<div className="absolute inset-x-0 top-1/2 h-px -translate-y-1/2 bg-gray-500" />
|
|
256
|
+
{label ? <DurationLabel label={label} /> : null}
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
225
261
|
function SegmentBar({ segments }: { segments: VisibleSegment[] }): ReactNode {
|
|
226
262
|
return (
|
|
227
263
|
<div className="relative h-6 w-full">
|
|
228
264
|
{segments.map((seg, i) => {
|
|
265
|
+
if (seg.status === 'queued') {
|
|
266
|
+
const leadInLabel = formatDurationPrecise(seg.fullDurationMs);
|
|
267
|
+
const showLeadInLabel =
|
|
268
|
+
seg.pixelWidth >= Math.max(40, leadInLabel.length * 6 + 12);
|
|
269
|
+
return (
|
|
270
|
+
<LeadInConnector
|
|
271
|
+
key={i}
|
|
272
|
+
leftPct={seg.leftPct}
|
|
273
|
+
widthPct={seg.widthPct}
|
|
274
|
+
label={showLeadInLabel ? leadInLabel : null}
|
|
275
|
+
/>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
229
279
|
const label = formatDurationPrecise(seg.fullDurationMs);
|
|
230
280
|
// Only render the label when there's enough room for it without clipping.
|
|
231
281
|
const showLabel = seg.pixelWidth >= Math.max(40, label.length * 6 + 12);
|
|
232
|
-
// Beef up the queued segment when it's too narrow to read.
|
|
233
|
-
const isNarrowQueued = seg.status === 'queued' && seg.pixelWidth < 20;
|
|
234
|
-
const overrideBg = isNarrowQueued ? 'var(--ds-gray-400)' : undefined;
|
|
235
|
-
const overrideBorder = isNarrowQueued
|
|
236
|
-
? 'var(--ds-gray-500)'
|
|
237
|
-
: undefined;
|
|
238
282
|
|
|
239
283
|
return (
|
|
240
284
|
<div
|
|
241
285
|
key={i}
|
|
242
286
|
className={cn(
|
|
243
|
-
'absolute h-full rounded-[0.25rem]',
|
|
287
|
+
'absolute h-full overflow-hidden rounded-[0.25rem]',
|
|
244
288
|
SEGMENT_CLASSES[seg.status]
|
|
245
289
|
)}
|
|
246
290
|
style={{
|
|
@@ -248,10 +292,11 @@ function SegmentBar({ segments }: { segments: VisibleSegment[] }): ReactNode {
|
|
|
248
292
|
left: `calc(${seg.leftPct}% + 0.5px)`,
|
|
249
293
|
width: `calc(${seg.widthPct}% - 1px)`,
|
|
250
294
|
minWidth: 1,
|
|
251
|
-
background: overrideBg,
|
|
252
|
-
borderColor: overrideBorder,
|
|
253
295
|
}}
|
|
254
296
|
>
|
|
297
|
+
{ACTIVE_SEGMENT_STATUSES.has(seg.status) ? (
|
|
298
|
+
<RunningStripes />
|
|
299
|
+
) : null}
|
|
255
300
|
{showLabel ? <DurationLabel label={label} /> : null}
|
|
256
301
|
</div>
|
|
257
302
|
);
|
|
@@ -260,6 +305,80 @@ function SegmentBar({ segments }: { segments: VisibleSegment[] }): ReactNode {
|
|
|
260
305
|
);
|
|
261
306
|
}
|
|
262
307
|
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// Event markers (attr_set)
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
function formatMarkerTime(ms: number): string {
|
|
313
|
+
const date = new Date(ms);
|
|
314
|
+
return (
|
|
315
|
+
date.toLocaleTimeString('en-US', {
|
|
316
|
+
hour: '2-digit',
|
|
317
|
+
minute: '2-digit',
|
|
318
|
+
second: '2-digit',
|
|
319
|
+
hour12: false,
|
|
320
|
+
}) +
|
|
321
|
+
'.' +
|
|
322
|
+
date.getMilliseconds().toString().padStart(3, '0')
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Diamond markers for `attr_set` events on a span's timeline row. Attribute
|
|
328
|
+
* changes are point-in-time events with no duration, so they don't get a
|
|
329
|
+
* status segment — instead they render as small teal diamonds at the moment
|
|
330
|
+
* the attributes were written.
|
|
331
|
+
*/
|
|
332
|
+
function AttrSetMarkers({
|
|
333
|
+
span,
|
|
334
|
+
viewStart,
|
|
335
|
+
viewDuration,
|
|
336
|
+
}: {
|
|
337
|
+
span: Span;
|
|
338
|
+
viewStart: number;
|
|
339
|
+
viewDuration: number;
|
|
340
|
+
}): ReactNode {
|
|
341
|
+
const markers = useMemo(
|
|
342
|
+
() => span.events.filter((e) => e.name === 'attr_set'),
|
|
343
|
+
[span.events]
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
if (markers.length === 0 || viewDuration <= 0) return null;
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<>
|
|
350
|
+
{markers.map((event, index) => {
|
|
351
|
+
const timeMs = getHighResInMs(event.timestamp);
|
|
352
|
+
const frac = (timeMs - viewStart) / viewDuration;
|
|
353
|
+
if (frac < 0 || frac > 1) return null;
|
|
354
|
+
|
|
355
|
+
return (
|
|
356
|
+
<Tooltip key={`attr-set-${index}`}>
|
|
357
|
+
<TooltipTrigger asChild>
|
|
358
|
+
<div
|
|
359
|
+
aria-label="Attributes set"
|
|
360
|
+
className="absolute top-1/2 z-10 flex h-4 w-4 -translate-x-1/2 -translate-y-1/2 cursor-pointer items-center justify-center"
|
|
361
|
+
style={{ left: `${frac * 100}%` }}
|
|
362
|
+
>
|
|
363
|
+
<div
|
|
364
|
+
className="h-2 w-2 rotate-45 rounded-[1px] border"
|
|
365
|
+
style={{
|
|
366
|
+
backgroundColor: 'var(--ds-teal-400)',
|
|
367
|
+
borderColor: 'var(--ds-teal-900)',
|
|
368
|
+
}}
|
|
369
|
+
/>
|
|
370
|
+
</div>
|
|
371
|
+
</TooltipTrigger>
|
|
372
|
+
<TooltipContent>
|
|
373
|
+
Attributes set · {formatMarkerTime(timeMs)}
|
|
374
|
+
</TooltipContent>
|
|
375
|
+
</Tooltip>
|
|
376
|
+
);
|
|
377
|
+
})}
|
|
378
|
+
</>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
263
382
|
// ---------------------------------------------------------------------------
|
|
264
383
|
// TimelineBar
|
|
265
384
|
// ---------------------------------------------------------------------------
|
|
@@ -359,6 +478,11 @@ const TimelineBar = memo(function TimelineBar({
|
|
|
359
478
|
/>
|
|
360
479
|
)}
|
|
361
480
|
</div>
|
|
481
|
+
<AttrSetMarkers
|
|
482
|
+
span={span}
|
|
483
|
+
viewStart={viewStart}
|
|
484
|
+
viewDuration={viewDuration}
|
|
485
|
+
/>
|
|
362
486
|
</div>
|
|
363
487
|
</div>
|
|
364
488
|
);
|
|
@@ -21,17 +21,23 @@ import {
|
|
|
21
21
|
} from 'react';
|
|
22
22
|
import { useLoadMoreOnScroll } from '../../hooks/use-load-more-on-scroll';
|
|
23
23
|
import { useReducedMotion } from '../../hooks/use-reduced-motion';
|
|
24
|
+
import { filterSpanRawEvents } from '../../lib/trace-builder';
|
|
24
25
|
import { ErrorBoundary } from '../error-boundary';
|
|
25
26
|
import {
|
|
26
27
|
EntityDetailPanel,
|
|
27
28
|
type SelectedSpanInfo,
|
|
28
29
|
} from '../sidebar/entity-detail-panel';
|
|
29
30
|
import { useSidebarData } from '../sidebar/sidebar-data-context';
|
|
30
|
-
import type { TraceWithMeta } from './types';
|
|
31
31
|
import { formatDuration, getHighResInMs } from '../trace-viewer/util/timing';
|
|
32
32
|
import { IconButton } from '../ui/icon-button';
|
|
33
|
-
import { Spinner } from '../ui/spinner';
|
|
34
33
|
import { Kbd } from '../ui/kbd';
|
|
34
|
+
import { Spinner } from '../ui/spinner';
|
|
35
|
+
import {
|
|
36
|
+
Tooltip,
|
|
37
|
+
TooltipContent,
|
|
38
|
+
TooltipProvider,
|
|
39
|
+
TooltipTrigger,
|
|
40
|
+
} from '../ui/tooltip';
|
|
35
41
|
import EventList from './components/event-list';
|
|
36
42
|
import { SplitPane } from './components/split-pane';
|
|
37
43
|
import {
|
|
@@ -39,14 +45,9 @@ import {
|
|
|
39
45
|
Timeline,
|
|
40
46
|
TimelineHeader,
|
|
41
47
|
} from './components/timeline';
|
|
42
|
-
import {
|
|
43
|
-
Tooltip,
|
|
44
|
-
TooltipContent,
|
|
45
|
-
TooltipProvider,
|
|
46
|
-
TooltipTrigger,
|
|
47
|
-
} from '../ui/tooltip';
|
|
48
48
|
import { ActiveSpanProvider, useActiveSpan } from './context';
|
|
49
49
|
import { searchSpans } from './search';
|
|
50
|
+
import type { TraceWithMeta } from './types';
|
|
50
51
|
import { computeRootBounds, computeTimeMarkers } from './utils';
|
|
51
52
|
|
|
52
53
|
interface NewTraceViewerProps {
|
|
@@ -145,14 +146,16 @@ function useSelectedSpanInfo(): SelectedSpanInfo | null {
|
|
|
145
146
|
return useMemo(() => {
|
|
146
147
|
if (!activeSpan) return null;
|
|
147
148
|
|
|
148
|
-
const
|
|
149
|
-
const rawEvents =
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
const resource = activeSpan.attributes?.resource as string | undefined;
|
|
150
|
+
const rawEvents = filterSpanRawEvents(
|
|
151
|
+
sidebar.events,
|
|
152
|
+
resource,
|
|
153
|
+
activeSpan.spanId
|
|
154
|
+
);
|
|
152
155
|
|
|
153
156
|
return {
|
|
154
157
|
data: activeSpan.attributes?.data,
|
|
155
|
-
resource
|
|
158
|
+
resource,
|
|
156
159
|
spanId: activeSpan.spanId,
|
|
157
160
|
rawEvents,
|
|
158
161
|
};
|
|
@@ -459,7 +462,7 @@ function NewTraceViewerContent({
|
|
|
459
462
|
)
|
|
460
463
|
);
|
|
461
464
|
const isMouseWheel = e.deltaMode === 1 || Math.abs(e.deltaY) >= 50;
|
|
462
|
-
const scaleFactor =
|
|
465
|
+
const scaleFactor = 2 ** (dy / (isMouseWheel ? 200 : 60));
|
|
463
466
|
|
|
464
467
|
setViewport((prev) => {
|
|
465
468
|
const prevDuration = prev.end - prev.start;
|
|
@@ -185,6 +185,7 @@ export function getResourceColor(resource: string): {
|
|
|
185
185
|
export type SegmentStatus =
|
|
186
186
|
| 'queued'
|
|
187
187
|
| 'running'
|
|
188
|
+
| 'completed'
|
|
188
189
|
| 'failed'
|
|
189
190
|
| 'retrying'
|
|
190
191
|
| 'succeeded'
|
|
@@ -373,6 +374,12 @@ function computeSleepSegmentsFromSpan(
|
|
|
373
374
|
return [{ startFraction: 0, endFraction: 1, status: 'sleeping' }];
|
|
374
375
|
}
|
|
375
376
|
|
|
377
|
+
function runSegmentStatus(runStatus: string | undefined): SegmentStatus {
|
|
378
|
+
if (runStatus === 'failed') return 'failed';
|
|
379
|
+
if (runStatus === 'running' || runStatus === 'pending') return 'running';
|
|
380
|
+
return 'completed';
|
|
381
|
+
}
|
|
382
|
+
|
|
376
383
|
function computeRunSegmentsFromSpan(
|
|
377
384
|
startMs: number,
|
|
378
385
|
duration: number,
|
|
@@ -387,11 +394,12 @@ function computeRunSegmentsFromSpan(
|
|
|
387
394
|
.map((e) => ({ name: e.name, time: getHighResInMs(e.timestamp) }))
|
|
388
395
|
.sort((a, b) => a.time - b.time);
|
|
389
396
|
|
|
397
|
+
const runData = attributes?.data as Record<string, unknown> | undefined;
|
|
398
|
+
const runStatus = runData?.status as string | undefined;
|
|
399
|
+
|
|
390
400
|
const hasRunCreated = sorted.some((e) => e.name === 'run_created');
|
|
391
401
|
|
|
392
402
|
if (!hasRunCreated) {
|
|
393
|
-
const runData = attributes?.data as Record<string, unknown> | undefined;
|
|
394
|
-
const runStatus = runData?.status as string | undefined;
|
|
395
403
|
return computeV1RunSegments(startMs, duration, activeStartMs, runStatus);
|
|
396
404
|
}
|
|
397
405
|
|
|
@@ -413,7 +421,7 @@ function computeRunSegmentsFromSpan(
|
|
|
413
421
|
segments.push({
|
|
414
422
|
startFraction: cursor,
|
|
415
423
|
endFraction: 1,
|
|
416
|
-
status: failedEvent ? 'failed' :
|
|
424
|
+
status: failedEvent ? 'failed' : runSegmentStatus(runStatus),
|
|
417
425
|
});
|
|
418
426
|
|
|
419
427
|
return segments;
|
|
@@ -443,7 +451,7 @@ function computeV1RunSegments(
|
|
|
443
451
|
segments.push({
|
|
444
452
|
startFraction: cursor,
|
|
445
453
|
endFraction: 1,
|
|
446
|
-
status: runStatus
|
|
454
|
+
status: runSegmentStatus(runStatus),
|
|
447
455
|
});
|
|
448
456
|
|
|
449
457
|
return segments;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name';
|
|
4
4
|
import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
|
|
5
5
|
import type { ModelMessage } from 'ai';
|
|
6
|
+
import { format } from 'date-fns';
|
|
6
7
|
import type { KeyboardEvent, ReactNode } from 'react';
|
|
7
8
|
import { useCallback, useContext, useMemo, useState } from 'react';
|
|
8
9
|
import { isEncryptedMarker, isExpiredMarker } from '../../lib/hydration';
|
|
@@ -10,6 +11,7 @@ import { useToast } from '../../lib/toast';
|
|
|
10
11
|
import { extractConversation, isDoStreamStep } from '../../lib/utils';
|
|
11
12
|
import { CopyButton } from '../new-trace-viewer/components/copy-button';
|
|
12
13
|
import { MiddleTruncate } from '../new-trace-viewer/components/middle-truncate/middle-truncate';
|
|
14
|
+
import { ContextCardProvider } from '../ui/context-card';
|
|
13
15
|
import {
|
|
14
16
|
DecryptClickContext,
|
|
15
17
|
RunClickContext,
|
|
@@ -19,6 +21,7 @@ import { ErrorCard } from '../ui/error-card';
|
|
|
19
21
|
import { ErrorStackBlock, isStructuredError } from '../ui/error-stack-block';
|
|
20
22
|
import { Skeleton } from '../ui/skeleton';
|
|
21
23
|
import { TimestampTooltip } from '../ui/timestamp-tooltip';
|
|
24
|
+
import { RunAttributesCard } from './attributes-block';
|
|
22
25
|
import { ConversationView } from './conversation-view';
|
|
23
26
|
import { CopyableDataBlock, EncryptedDataBlock } from './copyable-data-block';
|
|
24
27
|
import { DetailCard } from './detail-card';
|
|
@@ -339,15 +342,7 @@ const parseDateValue = (value: unknown): Date | null => {
|
|
|
339
342
|
};
|
|
340
343
|
|
|
341
344
|
const formatLocalMillisecondTime = (date: Date): string =>
|
|
342
|
-
date.
|
|
343
|
-
year: 'numeric',
|
|
344
|
-
month: 'numeric',
|
|
345
|
-
day: 'numeric',
|
|
346
|
-
hour: 'numeric',
|
|
347
|
-
minute: 'numeric',
|
|
348
|
-
second: 'numeric',
|
|
349
|
-
fractionalSecondDigits: 3,
|
|
350
|
-
});
|
|
345
|
+
format(date, 'MMM dd HH:mm:ss.SS OO').toUpperCase();
|
|
351
346
|
|
|
352
347
|
export const localMillisecondTime = (value: unknown): string => {
|
|
353
348
|
const date = parseDateValue(value);
|
|
@@ -355,7 +350,6 @@ export const localMillisecondTime = (value: unknown): string => {
|
|
|
355
350
|
return '-';
|
|
356
351
|
}
|
|
357
352
|
|
|
358
|
-
// e.g. 12/17/2025, 9:08:55.182 AM
|
|
359
353
|
return formatLocalMillisecondTime(date);
|
|
360
354
|
};
|
|
361
355
|
|
|
@@ -416,12 +410,12 @@ const attributeToDisplayFn: Record<
|
|
|
416
410
|
projectId: (_value: unknown) => null,
|
|
417
411
|
environment: (_value: unknown) => null,
|
|
418
412
|
executionContext: (_value: unknown) => null,
|
|
419
|
-
// Attributes
|
|
420
|
-
// Rendered in its own collapsible DetailCard;
|
|
421
|
-
// by the hasDisplayContent gate.
|
|
413
|
+
// Attributes — string-string metadata attached to the run.
|
|
414
|
+
// Rendered as key-value rows in its own collapsible DetailCard;
|
|
415
|
+
// if empty/missing, hidden by the hasDisplayContent gate.
|
|
422
416
|
attributes: (value: unknown) => {
|
|
423
417
|
if (!hasDisplayContent(value)) return null;
|
|
424
|
-
return <
|
|
418
|
+
return <RunAttributesCard attributes={value as Record<string, string>} />;
|
|
425
419
|
},
|
|
426
420
|
// Dates — wrapped with TimestampTooltip showing UTC/local + relative time
|
|
427
421
|
createdAt: timestampWithTooltipOrNull,
|
|
@@ -846,113 +840,117 @@ export const AttributePanel = ({
|
|
|
846
840
|
: outerDecryptCtx;
|
|
847
841
|
|
|
848
842
|
return (
|
|
849
|
-
<
|
|
850
|
-
<
|
|
851
|
-
<
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
{
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
843
|
+
<ContextCardProvider>
|
|
844
|
+
<RunClickContext.Provider value={onRunClick}>
|
|
845
|
+
<StreamClickContext.Provider value={onStreamClick}>
|
|
846
|
+
<DecryptClickContext.Provider value={decryptValue}>
|
|
847
|
+
{visibleBasicAttributes.length > 0 && (
|
|
848
|
+
<div className="flex flex-col overflow-hidden divide-y divide-gray-alpha-400 mb-3">
|
|
849
|
+
{orderedBasicAttributes.map((attribute) => {
|
|
850
|
+
const displayValue = attributeToDisplayFn[
|
|
851
|
+
attribute as keyof typeof attributeToDisplayFn
|
|
852
|
+
]?.(displayData[attribute as keyof typeof displayData]);
|
|
853
|
+
const isModuleSpecifier = attribute === 'moduleSpecifier';
|
|
854
|
+
const isCopyableBasicAttribute =
|
|
855
|
+
copyableBasicAttributes.has(attribute as AttributeKey) &&
|
|
856
|
+
typeof displayValue === 'string';
|
|
857
|
+
const moduleSpecifierValue =
|
|
858
|
+
typeof displayValue === 'string'
|
|
859
|
+
? displayValue
|
|
860
|
+
: String(
|
|
861
|
+
displayValue ?? displayData.moduleSpecifier ?? ''
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
return (
|
|
865
|
+
<div
|
|
866
|
+
className="flex items-center justify-between py-2"
|
|
867
|
+
key={attribute}
|
|
868
|
+
>
|
|
869
|
+
<span className="text-label-14 text-gray-900">
|
|
870
|
+
{getAttributeDisplayName(attribute)}
|
|
871
|
+
</span>
|
|
872
|
+
{isModuleSpecifier ? (
|
|
873
|
+
<button
|
|
874
|
+
type="button"
|
|
875
|
+
className="min-w-0 max-w-[70%] truncate text-right text-label-13 font-mono"
|
|
876
|
+
style={{
|
|
877
|
+
color: 'var(--ds-gray-1000)',
|
|
878
|
+
background: 'transparent',
|
|
879
|
+
border: 'none',
|
|
880
|
+
padding: 0,
|
|
881
|
+
}}
|
|
882
|
+
title={moduleSpecifierValue}
|
|
883
|
+
onClick={() =>
|
|
884
|
+
handleCopyModuleSpecifier(moduleSpecifierValue)
|
|
885
|
+
}
|
|
886
|
+
>
|
|
887
|
+
{moduleSpecifierValue}
|
|
888
|
+
</button>
|
|
889
|
+
) : isCopyableBasicAttribute ? (
|
|
890
|
+
<div
|
|
891
|
+
className="flex min-w-0 max-w-[70%] items-center justify-end gap-1 text-right text-[13px] font-mono"
|
|
892
|
+
style={{
|
|
893
|
+
color: 'var(--ds-gray-1000)',
|
|
894
|
+
}}
|
|
895
|
+
title={displayValue}
|
|
896
|
+
>
|
|
897
|
+
<MiddleTruncate
|
|
898
|
+
value={displayValue}
|
|
899
|
+
className="flex-1"
|
|
900
|
+
/>
|
|
901
|
+
<CopyButton
|
|
902
|
+
copyText={displayValue}
|
|
903
|
+
ariaLabel={`Copy ${getAttributeDisplayName(attribute)}`}
|
|
904
|
+
className="shrink-0 -mr-1"
|
|
905
|
+
/>
|
|
906
|
+
</div>
|
|
907
|
+
) : (
|
|
908
|
+
<span className="text-right text-label-13 font-mono">
|
|
909
|
+
{displayValue}
|
|
910
|
+
</span>
|
|
911
|
+
)}
|
|
912
|
+
</div>
|
|
913
|
+
);
|
|
914
|
+
})}
|
|
915
|
+
{isLoading && resource === 'sleep' && !displayData.resumeAt && (
|
|
916
|
+
<div className="py-1">
|
|
917
|
+
<div className="flex min-h-[32px] items-center justify-between gap-4 rounded-sm px-2.5 py-1">
|
|
918
|
+
<span
|
|
919
|
+
className="text-[14px] first-letter:uppercase"
|
|
920
|
+
style={{ color: 'var(--ds-gray-700)' }}
|
|
899
921
|
>
|
|
900
|
-
|
|
901
|
-
value={displayValue}
|
|
902
|
-
className="flex-1"
|
|
903
|
-
/>
|
|
904
|
-
<CopyButton
|
|
905
|
-
copyText={displayValue}
|
|
906
|
-
ariaLabel={`Copy ${getAttributeDisplayName(attribute)}`}
|
|
907
|
-
className="shrink-0 -mr-1"
|
|
908
|
-
/>
|
|
909
|
-
</div>
|
|
910
|
-
) : (
|
|
911
|
-
<span className="text-right text-label-13 font-mono">
|
|
912
|
-
{displayValue}
|
|
922
|
+
resumeAt
|
|
913
923
|
</span>
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
);
|
|
917
|
-
})}
|
|
918
|
-
{isLoading && resource === 'sleep' && !displayData.resumeAt && (
|
|
919
|
-
<div className="py-1">
|
|
920
|
-
<div className="flex min-h-[32px] items-center justify-between gap-4 rounded-sm px-2.5 py-1">
|
|
921
|
-
<span
|
|
922
|
-
className="text-[14px] first-letter:uppercase"
|
|
923
|
-
style={{ color: 'var(--ds-gray-700)' }}
|
|
924
|
-
>
|
|
925
|
-
resumeAt
|
|
926
|
-
</span>
|
|
927
|
-
<Skeleton className="h-4 w-[55%]" />
|
|
924
|
+
<Skeleton className="h-4 w-[55%]" />
|
|
925
|
+
</div>
|
|
928
926
|
</div>
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
</
|
|
955
|
-
</
|
|
956
|
-
</
|
|
927
|
+
)}
|
|
928
|
+
</div>
|
|
929
|
+
)}
|
|
930
|
+
{error ? (
|
|
931
|
+
<ErrorCard
|
|
932
|
+
title="Failed to load resource details"
|
|
933
|
+
details={error.message}
|
|
934
|
+
className="my-4"
|
|
935
|
+
/>
|
|
936
|
+
) : hasExpired ? (
|
|
937
|
+
<ExpiredDataMessage />
|
|
938
|
+
) : resolvedAttributes.length > 0 ? (
|
|
939
|
+
<>
|
|
940
|
+
{resolvedAttributes.map((attribute) => (
|
|
941
|
+
<AttributeBlock
|
|
942
|
+
isLoading={isLoading}
|
|
943
|
+
key={attribute}
|
|
944
|
+
attribute={attribute}
|
|
945
|
+
value={displayData[attribute as keyof typeof displayData]}
|
|
946
|
+
context={displayContext}
|
|
947
|
+
/>
|
|
948
|
+
))}
|
|
949
|
+
</>
|
|
950
|
+
) : null}
|
|
951
|
+
</DecryptClickContext.Provider>
|
|
952
|
+
</StreamClickContext.Provider>
|
|
953
|
+
</RunClickContext.Provider>
|
|
954
|
+
</ContextCardProvider>
|
|
957
955
|
);
|
|
958
956
|
};
|