Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
3ee6c2a
wip
mitul-s Mar 19, 2026
ee38ef1
context
mitul-s Mar 19, 2026
b8f0676
cleanup
mitul-s Mar 19, 2026
6ca75d7
wip
mitul-s Mar 19, 2026
b3ce9b7
wip
mitul-s Mar 19, 2026
977b278
Update trace-viewer.tsx
mitul-s Mar 19, 2026
0c3d416
adjust hovers
mitul-s Mar 19, 2026
a65d7bf
time marker
mitul-s Mar 19, 2026
f076a56
tweak
mitul-s Mar 19, 2026
6c92c5c
Update event-list.tsx
mitul-s Mar 19, 2026
7bc8e67
header
mitul-s Mar 19, 2026
f35331b
Update utils.ts
mitul-s Mar 19, 2026
5ef9a9b
search
mitul-s Mar 19, 2026
f74e80c
Update timeline.tsx
mitul-s Mar 19, 2026
1418a66
ship
mitul-s Mar 19, 2026
595b14f
Update timeline.tsx
mitul-s Mar 20, 2026
3ce1c8a
Update event-list.tsx
mitul-s Mar 20, 2026
a6514a0
Create icons.tsx
mitul-s Mar 20, 2026
a905c3d
Update trace-viewer.tsx
mitul-s Mar 20, 2026
202b5b5
Update utils.ts
mitul-s Mar 20, 2026
37a045d
Merge branch 'main' into ms/trace-viewer-2
mitul-s Mar 20, 2026
ffcebde
new trace viewrr
mitul-s Mar 20, 2026
6f42f16
Update workflow-trace-view.tsx
mitul-s Mar 20, 2026
e7fd6b5
Fix: Runtime crash when `trace` is `undefined`: `NewTraceViewerCompon…
vercel[bot] Mar 20, 2026
7a279f1
Merge branch 'main' into ms/trace-viewer-2
mitul-s Mar 20, 2026
ecb6fd0
Merge branch 'main' into ms/trace-viewer-2
mitul-s Mar 30, 2026
970233a
fixed edge view
mitul-s Mar 30, 2026
071857c
Update timeline.tsx
mitul-s Mar 30, 2026
956639d
ship it
mitul-s Mar 31, 2026
9ccab96
cleanup
mitul-s Mar 31, 2026
7786d71
Update trace-viewer.tsx
mitul-s Mar 31, 2026
bfe2a8c
wip
mitul-s Apr 9, 2026
e073494
wip
mitul-s Apr 9, 2026
701fd61
Merge branch 'main' into ms/trace-viewer-2
mitul-s Apr 9, 2026
047a854
Add detail pane to the new trace viewer + cleanup (#1714)
mitul-s Apr 13, 2026
260fc20
Merge branch 'main' into ms/trace-viewer-2
mitul-s Apr 14, 2026
1f081bf
cleanu
mitul-s Apr 14, 2026
95a9e05
Merge branch 'main' into ms/trace-viewer-2
mitul-s Apr 14, 2026
6e484f9
fix
mitul-s Apr 14, 2026
175427c
Merge branch 'ms/trace-viewer-2' of https://github.com/vercel/workflo…
mitul-s Apr 14, 2026
469c12c
Update workflow-trace-view.tsx
mitul-s Apr 14, 2026
6df48fd
cleanup
mitul-s Apr 14, 2026
d54866b
Merge branch 'main' into ms/trace-viewer-2
mitul-s Apr 14, 2026
ca89088
Update entity-detail-panel.tsx
mitul-s Apr 14, 2026
95bbd43
Merge branch 'main' into ms/trace-viewer-2
VaguelySerious Apr 14, 2026
8279b43
move sidebar provider into export
mitul-s Apr 15, 2026
d3b53b4
Merge branch 'main' into ms/trace-viewer-2
mitul-s Apr 15, 2026
58085e9
dead code
mitul-s Apr 16, 2026
7651a53
Merge branch 'main' into ms/trace-viewer-2
mitul-s Apr 16, 2026
1898bcd
cleaning up more
mitul-s Apr 16, 2026
e463f13
Merge branch 'main' into ms/trace-viewer-2
mitul-s Apr 16, 2026
9f5877f
remove decorative indenting & output loader
mitul-s Apr 16, 2026
cbfa9c4
fix bars
mitul-s Apr 16, 2026
71c1257
Update inspector-theme.ts
mitul-s Apr 16, 2026
3f907db
cleanups
mitul-s Apr 21, 2026
39d9890
Update copyable-data-block.tsx
mitul-s Apr 21, 2026
3dbe299
Merge branch 'main' into ms/trace-viewer-2
mitul-s Apr 22, 2026
7aefc52
cleanup
mitul-s Apr 29, 2026
5cb758b
chonky
mitul-s Apr 29, 2026
a3b14d5
Update timeline.tsx
mitul-s Apr 29, 2026
b0dca2c
bug fxi
mitul-s Apr 29, 2026
4038f33
marker lines
mitul-s Apr 29, 2026
62e9a64
wip
mitul-s Apr 29, 2026
9a90c06
changes
mitul-s Apr 29, 2026
cd6e5db
Update event-list.tsx
mitul-s Apr 29, 2026
1abcf34
rounded
mitul-s Apr 29, 2026
2497e23
Merge branch 'main' into ms/trace-without-compression
mitul-s Apr 30, 2026
7df11f1
Update event-list.tsx
mitul-s Apr 30, 2026
c3494a0
middle truncate component
mitul-s Apr 30, 2026
f5adc82
cleanup
mitul-s Apr 30, 2026
ccacb74
height updatres
mitul-s Apr 30, 2026
750b66b
colour fixes
mitul-s Apr 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Circle } from 'lucide-react';
import { cn } from '../../../lib/utils';
import type { Span } from '../../trace-viewer/types';
import { formatDuration, getHighResInMs } from '../../trace-viewer/util/timing';
import { formatDuration } from '../../trace-viewer/util/timing';
import {
WorkflowIcon,
WebhookIcon,
SleepIcon,
StepForwardIcon,
} from '../icons';
import { getSpanDurationMs } from '../utils';
import { MiddleTruncate } from './middle-truncate/middle-truncate';

interface EventStyle {
icon: React.ComponentType<{ className?: string }>;
Expand All @@ -26,6 +28,8 @@ const defaultStyle: EventStyle = {
className: 'text-gray-900',
};

const ROW_HEIGHT_CLASS = 'h-10';

function getEventStyle(resource: string, isErrored: boolean): EventStyle {
const style = eventStyles[resource] ?? defaultStyle;
return {
Expand All @@ -43,7 +47,7 @@ const EventRow = ({
isSelected: boolean;
onSelectSpan: (spanId: string) => void;
}) => {
const durationMs = getHighResInMs(span.duration);
const durationMs = getSpanDurationMs(span);
const isErrored =
(span.attributes.data as Record<string, unknown>).status === 'failed';
const { icon: Icon, className: tagClassName } = getEventStyle(
Expand All @@ -53,24 +57,31 @@ const EventRow = ({

return (
<li
className="overflow-clip group"
role="treeitem"
className={cn(
'relative overflow-clip group after:absolute after:inset-x-0 after:bottom-0 after:h-px after:bg-gray-alpha-400',
ROW_HEIGHT_CLASS
)}
role='treeitem'
aria-selected={isSelected}
aria-expanded={isSelected}
aria-level={1}
onClick={() => onSelectSpan(span.spanId)}
>
<div className="hover:bg-gray-100 group-aria-selected:bg-gray-100 group-aria-selected:hover:bg-gray-200 hover:aria-selected:bg-gray-100 rounded-sm px-2 h-[34px] py-1.5 flex">
<div className="flex items-center gap-2">
<span className={tagClassName}>
<Icon className="w-4 h-4" />
</span>
<span className="text-label-14">{span.name}</span>
</div>
<div className="ml-auto">
<span className="text-label-14 text-gray-900 tabular-nums">
{formatDuration(durationMs)}
</span>
<div className='h-full hover:bg-gray-100 group-aria-selected:bg-gray-100 group-aria-selected:hover:bg-gray-200'>
<div className='flex h-full min-w-0 items-center px-2'>
<div className='flex min-w-0 flex-1 items-center gap-2'>
<span className={cn('shrink-0', tagClassName)}>
<Icon className='w-4 h-4' />
</span>
<span className='min-w-0 text-label-14'>
<MiddleTruncate value={span.name} />
</span>
</div>
<div className='ml-2 shrink-0'>
<span className='text-label-14 text-gray-900 tabular-nums'>
{formatDuration(durationMs)}
</span>
</div>
</div>
</div>
</li>
Expand All @@ -87,11 +98,7 @@ const EventList = ({
onSelectSpan: (spanId: string) => void;
}) => {
return (
<ul
id="event-list"
role="tree"
className="block min-h-0 overflow-visible px-2 py-2"
>
<ul id='event-list' role='tree' className='block min-h-0 overflow-visible'>
{spans.map((span) => {
return (
<EventRow
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { ELLIPSIS, toGraphemes } from './truncate';

interface GetMiddleTruncateCopyTextOptions {
prefixText: string;
selectionEnd: number;
selectionStart: number;
suffixText: string;
value: string;
}

interface GetMiddleTruncateCopyTextFromSelectionTextOptions {
prefixText: string;
selectionText: string;
suffixText: string;
value: string;
}

function getMiddleTruncateCopyText({
prefixText,
selectionEnd,
selectionStart,
suffixText,
value,
}: GetMiddleTruncateCopyTextOptions): string | null {
const visibleText = prefixText + ELLIPSIS + suffixText;

if (
selectionStart < 0 ||
selectionEnd > visibleText.length ||
selectionStart >= selectionEnd
) {
return null;
}

if (selectionStart === 0 && selectionEnd === visibleText.length) {
return value;
}

const ellipsisStart = prefixText.length;
const ellipsisEnd = ellipsisStart + ELLIPSIS.length;

if (selectionStart > ellipsisStart || selectionEnd < ellipsisEnd) {
return null;
}

const originalGraphemes = toGraphemes(value);
const selectedPrefixGraphemeCount = toGraphemes(
visibleText.slice(0, selectionStart)
).length;
const selectedSuffixGraphemeCount = toGraphemes(
visibleText.slice(ellipsisEnd, selectionEnd)
).length;
const suffixStart = originalGraphemes.length - toGraphemes(suffixText).length;

return originalGraphemes
.slice(
selectedPrefixGraphemeCount,
suffixStart + selectedSuffixGraphemeCount
)
.join('');
}

function getMiddleTruncateCopyTextFromSelectionText({
prefixText,
selectionText,
suffixText,
value,
}: GetMiddleTruncateCopyTextFromSelectionTextOptions): string | null {
const visibleText = prefixText + ELLIPSIS + suffixText;
const trimmedSelectionText = selectionText.trim();

if (!trimmedSelectionText) {
return null;
}

const leading = selectionText.slice(
0,
selectionText.length - selectionText.trimStart().length
);
const trailing = selectionText.slice(selectionText.trimEnd().length);

if (trimmedSelectionText === visibleText) {
return leading + value + trailing;
}

if (!trimmedSelectionText.includes(ELLIPSIS)) {
return null;
}

const selectionStart = visibleText.indexOf(trimmedSelectionText);
if (selectionStart === -1) {
return null;
}

const selectionEnd = selectionStart + trimmedSelectionText.length;
const mappedText = getMiddleTruncateCopyText({
prefixText,
selectionEnd,
selectionStart,
suffixText,
value,
});

return mappedText === null ? null : leading + mappedText + trailing;
}

export {
getMiddleTruncateCopyText,
getMiddleTruncateCopyTextFromSelectionText,
};
export type {
GetMiddleTruncateCopyTextFromSelectionTextOptions,
GetMiddleTruncateCopyTextOptions,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
'use client';

import { useCallback, useRef } from 'react';
import type { ClipboardEvent, ComponentPropsWithoutRef, JSX } from 'react';
import { cn } from '../../../../lib/utils';
import {
getMiddleTruncateCopyText,
getMiddleTruncateCopyTextFromSelectionText,
} from './copy-selection';
import { ELLIPSIS } from './truncate';
import { useMiddleTruncate } from './use-middle-truncate';

/** Props for the {@link MiddleTruncate} component. */
interface MiddleTruncateProps
extends Omit<ComponentPropsWithoutRef<'span'>, 'children'> {
/** The full text string to display, truncated from the middle when it overflows. */
value: string;
}

function getRangeOffsets(
container: HTMLElement,
range: Range
): { end: number; start: number } | null {
if (
!container.contains(range.startContainer) ||
!container.contains(range.endContainer)
) {
return null;
}

const startRange = document.createRange();
startRange.selectNodeContents(container);
startRange.setEnd(range.startContainer, range.startOffset);

const endRange = document.createRange();
endRange.selectNodeContents(container);
endRange.setEnd(range.endContainer, range.endOffset);

return {
end: endRange.toString().length,
start: startRange.toString().length,
};
}

/**
* @gdoc
*
* Truncates text from the middle with an ellipsis when it overflows, preserving the beginning and end (e.g. file paths, URLs). Copy behavior restores the full untruncated text to the clipboard.
* Renders a `<span>` element.
*
* Documentation: [Geist Middle Truncate](https://vercel.com/geist/middle-truncate)
*
* @param value - Full text string to display.
*/
function MiddleTruncate({
value,
className,
onCopy: onCopyProp,
...props
}: MiddleTruncateProps): JSX.Element {
const { ref, measureRef, displayText, isTruncated, prefixText, suffixText } =
useMiddleTruncate(value);
const visibleRef = useRef<HTMLSpanElement | null>(null);

const handleCopy = useCallback(
(e: ClipboardEvent<HTMLSpanElement>) => {
onCopyProp?.(e);
if (e.defaultPrevented || !isTruncated) return;

const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return;
const selectionText = selection.toString();
if (!selectionText) return;

// Only intercept when the selection is fully within this element.
// If the user selected across elements (e.g. a table row), let
// the browser's default behavior preserve surrounding context.
const range = selection.getRangeAt(0);
const visibleEl = visibleRef.current;
let copyText: string | null = null;

if (
e.currentTarget.contains(range.startContainer) &&
e.currentTarget.contains(range.endContainer) &&
visibleEl
) {
const offsets = getRangeOffsets(visibleEl, range);
if (offsets) {
copyText = getMiddleTruncateCopyText({
prefixText,
selectionEnd: offsets.end,
selectionStart: offsets.start,
suffixText,
value,
});
}
}

copyText ??= getMiddleTruncateCopyTextFromSelectionText({
prefixText,
selectionText,
suffixText,
value,
});

if (copyText === null) return;

e.preventDefault();
e.clipboardData.setData('text/plain', copyText);
},
[onCopyProp, isTruncated, prefixText, suffixText, value]
);

return (
<span
title={isTruncated ? value : undefined}
{...props}
ref={ref}
className={cn(
'relative inline-grid min-w-0 max-w-full overflow-hidden whitespace-nowrap',
className
)}
onCopy={handleCopy}
>
{isTruncated && <span className='sr-only select-none'>{value}</span>}
<span
aria-hidden='true'
className='pointer-events-none col-start-1 row-start-1 invisible select-none whitespace-nowrap'
>
{value}
</span>
<span
aria-hidden={isTruncated || undefined}
className='col-start-1 row-start-1 min-w-0 overflow-hidden'
ref={visibleRef}
>
{isTruncated ? (
<>
<span>{prefixText}</span>
<span>{ELLIPSIS}</span>
<span>{suffixText}</span>
</>
) : (
displayText
)}
</span>
<span
aria-hidden='true'
className='pointer-events-none absolute left-0 top-0 inline-block invisible select-none whitespace-nowrap'
ref={measureRef}
/>
</span>
);
}

export { MiddleTruncate };
export type { MiddleTruncateProps };
Loading
Loading