From 3ee6c2aa2046892552530c84554be577b09ef422 Mon Sep 17 00:00:00 2001 From: mitul-s Date: Thu, 19 Mar 2026 14:42:14 -0400 Subject: [PATCH 01/58] wip --- .../components/event-list.tsx | 77 +++ .../new-trace-viewer/components/timeline.tsx | 207 ++++++++ .../new-trace-viewer/trace-viewer.tsx | 107 ++++ .../src/components/new-trace-viewer/types.ts | 0 .../new-trace-viewer/util/constants.ts | 0 .../src/components/workflow-trace-view.tsx | 2 + packages/web-shared/src/styles.css | 460 +++++++++++++++++- 7 files changed, 852 insertions(+), 1 deletion(-) create mode 100644 packages/web-shared/src/components/new-trace-viewer/components/event-list.tsx create mode 100644 packages/web-shared/src/components/new-trace-viewer/components/timeline.tsx create mode 100644 packages/web-shared/src/components/new-trace-viewer/trace-viewer.tsx create mode 100644 packages/web-shared/src/components/new-trace-viewer/types.ts create mode 100644 packages/web-shared/src/components/new-trace-viewer/util/constants.ts diff --git a/packages/web-shared/src/components/new-trace-viewer/components/event-list.tsx b/packages/web-shared/src/components/new-trace-viewer/components/event-list.tsx new file mode 100644 index 0000000000..b88855bb2c --- /dev/null +++ b/packages/web-shared/src/components/new-trace-viewer/components/event-list.tsx @@ -0,0 +1,77 @@ +import { StepForward } from 'lucide-react'; +import { cva } from 'class-variance-authority'; +import { Span } from '../../trace-viewer/types'; +import { formatDuration } from '../../../lib/utils'; + +const eventTag = cva(['rounded-sm p-1'], { + variants: { + eventType: { + run: 'bg-blue-200 text-blue-900', + step: 'bg-green-200 text-green-900', + hook: 'bg-yellow-200 text-yellow-900', + sleep: 'bg-gray-200 text-gray-900', + default: 'bg-gray-200 text-gray-900', + }, + }, +}); + +type EventType = 'run' | 'step' | 'hook' | 'sleep' | 'default'; + +const toEventType = (resource: string): EventType => { + switch (resource) { + case 'run': + case 'step': + case 'hook': + case 'sleep': + return resource; + default: + return 'default'; + } +}; + +const EventRow = ({ span }: { span: Span }) => { + return ( + + ); +}; + +const EventList = ({ spans }: { spans: Span[] }) => { + return ( + + ); +}; + +export default EventList; diff --git a/packages/web-shared/src/components/new-trace-viewer/components/timeline.tsx b/packages/web-shared/src/components/new-trace-viewer/components/timeline.tsx new file mode 100644 index 0000000000..4fb1bbc14b --- /dev/null +++ b/packages/web-shared/src/components/new-trace-viewer/components/timeline.tsx @@ -0,0 +1,207 @@ +import { cva } from 'class-variance-authority'; +import { memo, type ReactNode } from 'react'; +import { cn } from '../../../lib/utils'; +import { SegmentStatus } from '../../trace-viewer/components/span-segments'; +import type { ResourceType } from '../../trace-viewer/components/span-strategies'; +import type { Span } from '../../trace-viewer/types'; +import { formatDuration, getHighResInMs } from '../../trace-viewer/util/timing'; + +const MIN_BAR_WIDTH_PCT = 0.8; + +export interface TimelineCompression { + toVisual: (time: number) => number; +} + +const clamp = (value: number, min: number, max: number): number => { + return Math.min(Math.max(value, min), max); +}; + +const toResourceType = (resource: string): ResourceType => { + switch (resource) { + case 'run': + case 'step': + case 'hook': + case 'sleep': + return resource; + default: + return 'default'; + } +}; + +const TimelineRow = ({ + children, + isSelected, + onClick, + title, +}: { + children: ReactNode; + isSelected: boolean; + onClick: () => void; + title: string; +}) => { + return ( + + ); +}; + +export const resourceStatus = cva('', { + variants: { + resourceType: { + run: 'bg-blue-200 text-blue-900', + step: 'bg-green-200 text-green-900', + hook: 'bg-amber-200 text-amber-900', + sleep: 'bg-purple-200 text-purple-900', + default: 'bg-gray-200 text-gray-900', + }, + errored: { + true: 'bg-red-200 text-red-900', + false: '', + }, + }, + defaultVariants: { + resourceType: 'default', + }, +}); + +const spanVariants = cva('relative block h-full w-full min-w-0.5 rounded-xs', { + variants: { + status: { + running: 'bg-green-700', + failed: 'bg-red-700', + succeeded: 'bg-blue-700', + retrying: 'bg-yellow-700', + queued: 'bg-gray-500', + waiting: 'bg-gray-700', + sleeping: 'bg-amber-700', + received: 'bg-blue-700', + }, + errored: { + true: 'bg-red-700', + false: '', + }, + resourceType: { + run: 'bg-blue-700', + step: 'bg-green-700', + hook: 'bg-amber-700', + sleep: 'bg-purple-700', + default: 'bg-gray-500', + }, + }, +}); + +export const TimelineBar = memo(function TimelineBar({ + span, + compression, + isSelected, + onClick, +}: { + span: Span; + compression: TimelineCompression; + isSelected: boolean; + onClick: () => void; +}): ReactNode { + const startTime = getHighResInMs(span.startTime); + const endTime = getHighResInMs(span.endTime); + const activeStartTime = span.activeStartTime + ? getHighResInMs(span.activeStartTime) + : undefined; + + const leftFrac = compression.toVisual(startTime); + const rightFrac = compression.toVisual(endTime); + const widthFrac = Math.max(rightFrac - leftFrac, 0); + + const leftPct = clamp(leftFrac * 100, 0, 100); + const maxWidthPct = Math.max(100 - leftPct, MIN_BAR_WIDTH_PCT); + const widthPct = + widthFrac > 0 + ? clamp(widthFrac * 100, MIN_BAR_WIDTH_PCT, maxWidthPct) + : MIN_BAR_WIDTH_PCT; + + const hasQueued = + activeStartTime != null && + activeStartTime > startTime && + activeStartTime < endTime; + + let queuedBarPct = 0; + let activeBarPct = 100; + if (hasQueued && widthFrac > 0) { + const activeFrac = compression.toVisual(activeStartTime); + queuedBarPct = clamp(((activeFrac - leftFrac) / widthFrac) * 100, 0, 100); + activeBarPct = 100 - queuedBarPct; + } + + const isErrored = span.status.code === 2; + const activeStatus: SegmentStatus = isErrored ? 'failed' : 'running'; + const durationLabel = formatDuration(Math.max(endTime - startTime, 0), true); + + return ( + +
+
+ {hasQueued && queuedBarPct > 0 ? ( + + ) : null} + +
+
+
+ ); +}); + +const Span = ({ + status, + resourceType, + width, +}: { + status: SegmentStatus; + resourceType: ResourceType; + width: number; +}) => { + return ( + + + + ); +}; + +export const Timeline = memo(function Timeline({ + children, +}: { + children: ReactNode; +}): ReactNode { + return
{children}
; +}); diff --git a/packages/web-shared/src/components/new-trace-viewer/trace-viewer.tsx b/packages/web-shared/src/components/new-trace-viewer/trace-viewer.tsx new file mode 100644 index 0000000000..03496ad800 --- /dev/null +++ b/packages/web-shared/src/components/new-trace-viewer/trace-viewer.tsx @@ -0,0 +1,107 @@ +'use client'; + +// import { cn } from "../../lib/utils"; +import EventList from './components/event-list'; +import type { Trace } from '../trace-viewer/types'; +import { useMemo, type ReactNode } from 'react'; +import { X } from 'lucide-react'; +import { Timeline, TimelineBar } from './components/timeline'; +import { getHighResInMs } from '../trace-viewer/util/timing'; + +interface NewTraceViewerProps { + trace: Trace; +} + +const TraceHeader = () => { + return ( +
+

Trace

+
+ ); +}; + +export function NewTraceViewer({ trace }: NewTraceViewerProps): ReactNode { + const activeSpan = trace.spans[0]; + const splitRatio = 0.5; + const compression = useMemo(() => { + let minStart = Number.POSITIVE_INFINITY; + let maxEnd = Number.NEGATIVE_INFINITY; + + for (const span of trace.spans) { + const start = getHighResInMs(span.startTime); + const end = getHighResInMs(span.endTime); + + if (start < minStart) minStart = start; + if (end > maxEnd) maxEnd = end; + } + + if (!Number.isFinite(minStart) || !Number.isFinite(maxEnd)) { + minStart = 0; + maxEnd = 1; + } + + const range = Math.max(maxEnd - minStart, 1); + + return { + toVisual(time: number): number { + return Math.min(Math.max((time - minStart) / range, 0), 1); + }, + }; + }, [trace.spans]); + + return ( +
+
+ +
+ +
+ + {trace.spans.map((span) => ( + {}} + /> + ))} + +
+
+ {activeSpan ? ( + <> +
+ + + ) : null} +
+ ); +} diff --git a/packages/web-shared/src/components/new-trace-viewer/types.ts b/packages/web-shared/src/components/new-trace-viewer/types.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/web-shared/src/components/new-trace-viewer/util/constants.ts b/packages/web-shared/src/components/new-trace-viewer/util/constants.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/web-shared/src/components/workflow-trace-view.tsx b/packages/web-shared/src/components/workflow-trace-view.tsx index 9136fe327d..f29e746fdf 100644 --- a/packages/web-shared/src/components/workflow-trace-view.tsx +++ b/packages/web-shared/src/components/workflow-trace-view.tsx @@ -37,6 +37,7 @@ import { getCustomSpanEventClassName, } from './workflow-traces/trace-colors'; import { buildTrace, type TraceWithMeta } from '../lib/trace-builder'; +import { NewTraceViewer } from './new-trace-viewer/trace-viewer'; /** * While a run is live, continuously grow root.duration and rescale so the @@ -966,6 +967,7 @@ export const WorkflowTraceViewer = ({ return (
+ {/* Timeline (takes remaining space) */}
strong]:font-medium [&>strong]:text-gray-900 text-[32px] leading-[40px] tracking-[-1.28px]; +} + +@utility text-heading-24 { + @apply font-sans font-semibold [&>strong]:font-medium [&>strong]:text-gray-900 text-[24px] leading-[32px] tracking-[-0.96px]; +} + +@utility text-heading-20 { + @apply font-sans font-semibold [&>strong]:font-medium [&>strong]:text-gray-900 text-[20px] leading-[26px] tracking-[-0.4px]; +} + +@utility text-heading-16 { + @apply font-sans font-semibold [&>strong]:font-medium [&>strong]:text-gray-900 text-[16px] leading-[24px] tracking-[-0.32px]; +} + +@utility text-heading-14 { + @apply font-sans font-semibold text-[14px] leading-[20px] tracking-[-0.28px]; +} + +/* Typography Button */ + +@utility text-button-16 { + @apply font-sans font-medium text-[16px] leading-[20px]; +} + +@utility text-button-14 { + @apply font-sans font-medium text-[14px] leading-[20px]; +} + +@utility text-button-12 { + @apply font-sans font-medium text-[12px] leading-[16px]; +} + +/* Typography Label */ + +@utility text-label-20 { + @apply font-sans font-normal text-[20px] leading-[32px]; +} + +@utility text-label-18 { + @apply font-sans font-normal text-[18px] leading-[20px]; +} + +@utility text-label-16 { + @apply font-sans font-normal [&>strong]:font-medium [&>strong]:text-gray-1000 text-[16px] leading-[20px]; +} + +@utility text-label-14 { + @apply font-sans font-normal [&>strong]:font-medium [&>strong]:text-gray-1000 text-[14px] leading-[20px]; +} + +@utility text-label-14-mono { + @apply font-mono font-normal text-[14px] leading-[20px]; +} + +@utility text-label-13 { + @apply font-sans font-normal [&>strong]:font-medium [&>strong]:text-gray-1000 text-[13px] leading-[16px]; +} + +@utility text-label-13-mono { + @apply font-mono font-normal text-[13px] leading-[20px]; +} + +@utility text-label-12 { + @apply font-sans font-normal [&>strong]:font-medium [&>strong]:text-gray-1000 text-[12px] leading-[16px]; +} + +@utility text-label-12-mono { + @apply font-mono font-normal text-[12px] leading-[16px]; +} + +/* Typography Copy */ + +@utility text-copy-24 { + @apply font-sans font-normal [&>strong]:font-medium [&>strong]:text-gray-1000 text-[24px] leading-[36px]; +} + +@utility text-copy-20 { + @apply font-sans font-normal [&>strong]:font-medium [&>strong]:text-gray-1000 text-[20px] leading-[36px]; +} + +@utility text-copy-18 { + @apply font-sans font-normal [&>strong]:font-medium [&>strong]:text-gray-1000 text-[18px] leading-[28px]; +} + +@utility text-copy-16 { + @apply font-sans font-normal [&>strong]:font-medium [&>strong]:text-gray-1000 text-[16px] leading-[24px]; +} + +@utility text-copy-14 { + @apply font-sans font-normal [&>strong]:font-medium [&>strong]:text-gray-1000 text-[14px] leading-[20px]; +} + +@utility text-copy-14-mono { + @apply font-mono font-normal [&>strong]:font-medium [&>strong]:text-gray-1000 text-[14px] leading-[20px]; +} + +@utility text-copy-13 { + @apply font-sans font-normal [&>strong]:font-medium [&>strong]:text-gray-1000 text-[13px] leading-[18px]; +} + +@utility text-copy-13-mono { + @apply font-mono font-normal text-[13px] leading-[18px]; +} + +@utility container { + @apply px-6 mx-auto w-full; + max-width: calc(1200px + 48px); +} From ee38ef1af2f72f1bddd7d2445cfc09e1572c77b4 Mon Sep 17 00:00:00 2001 From: mitul-s Date: Thu, 19 Mar 2026 14:45:48 -0400 Subject: [PATCH 02/58] context --- .../components/event-list.tsx | 45 ++++++--- .../components/new-trace-viewer/context.tsx | 94 +++++++++++++++++++ .../new-trace-viewer/trace-viewer.tsx | 27 ++++-- 3 files changed, 146 insertions(+), 20 deletions(-) create mode 100644 packages/web-shared/src/components/new-trace-viewer/context.tsx diff --git a/packages/web-shared/src/components/new-trace-viewer/components/event-list.tsx b/packages/web-shared/src/components/new-trace-viewer/components/event-list.tsx index b88855bb2c..ad9c84cd99 100644 --- a/packages/web-shared/src/components/new-trace-viewer/components/event-list.tsx +++ b/packages/web-shared/src/components/new-trace-viewer/components/event-list.tsx @@ -1,7 +1,7 @@ import { StepForward } from 'lucide-react'; import { cva } from 'class-variance-authority'; -import { Span } from '../../trace-viewer/types'; -import { formatDuration } from '../../../lib/utils'; +import type { Span } from '../../trace-viewer/types'; +import { formatDuration, getHighResInMs } from '../../trace-viewer/util/timing'; const eventTag = cva(['rounded-sm p-1'], { variants: { @@ -29,19 +29,25 @@ const toEventType = (resource: string): EventType => { } }; -const EventRow = ({ span }: { span: Span }) => { +const EventRow = ({ + span, + isSelected, + onSelectSpan, +}: { + span: Span; + isSelected: boolean; + onSelectSpan: (spanId: string) => void; +}) => { + const durationMs = getHighResInMs(span.duration); + return (