Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
ad8b7f1
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 27, 2026
e0cb61b
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 28, 2026
051cadc
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 29, 2026
af8ac1f
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 29, 2026
e68e7d2
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 30, 2026
8305e5b
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 30, 2026
f3da688
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 31, 2026
f8ee413
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 1, 2026
589cbd7
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 2, 2026
e979fdf
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 3, 2026
d4baed2
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 6, 2026
53503a4
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 10, 2026
d51e2e4
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 13, 2026
5123088
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 13, 2026
dd1a307
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 13, 2026
00bcfcd
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 14, 2026
f6a157b
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 15, 2026
816f35b
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 16, 2026
2b87dce
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 17, 2026
146de28
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 18, 2026
0ef6455
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 18, 2026
2d8c690
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 18, 2026
3513471
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 20, 2026
0f5d29e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 20, 2026
c76001e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 23, 2026
f97184f
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 24, 2026
f71c531
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 25, 2026
530e598
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 25, 2026
ea38511
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 27, 2026
19b1a3f
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 2, 2026
757bfdb
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 2, 2026
5fb4cfb
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 3, 2026
cb5adc3
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 3, 2026
046fdbc
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 3, 2026
2a7884f
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 3, 2026
60ecae6
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 5, 2026
71a6e95
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 6, 2026
25fb139
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 9, 2026
8fe6a36
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 9, 2026
d7cb1a3
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 10, 2026
99b34fe
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 10, 2026
e63bfca
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 11, 2026
810af84
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 11, 2026
67142f9
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 11, 2026
58b47e7
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 12, 2026
4dfac66
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 12, 2026
a423b68
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 13, 2026
b8f1f2e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 13, 2026
30fe808
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 13, 2026
972c20b
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 13, 2026
29eb1f9
Update event list to show decrypt button when a row is expanded
karthikscale3 Mar 13, 2026
c126b5d
improve loading skeleton
karthikscale3 Mar 13, 2026
740fd37
add timestamp tooltip
karthikscale3 Mar 13, 2026
0fe7fc0
add a toast adapter
karthikscale3 Mar 13, 2026
3d867f3
Merge branch 'main' of github.com:vercel/workflow into karthik/workfl…
karthikscale3 Mar 13, 2026
9278229
add changeset
karthikscale3 Mar 13, 2026
4566adf
remove noop timestamp tool tip
karthikscale3 Mar 13, 2026
8cebce6
add toast for decryption
karthikscale3 Mar 13, 2026
60d42ea
add toast for decryption
karthikscale3 Mar 13, 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
7 changes: 7 additions & 0 deletions .changeset/spotty-lemons-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@workflow/web-shared": patch
"@workflow/web": patch
---

web-shared: Timestamp tooltips, toast adapter, improved skeletons, and encrypted data detection for lazy-loaded events
web: Add toast for decryption
160 changes: 125 additions & 35 deletions packages/web-shared/src/components/event-list-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { LoadMoreButton } from './ui/load-more-button';
import { MenuDropdown } from './ui/menu-dropdown';
import { Skeleton } from './ui/skeleton';
import { TimestampTooltip } from './ui/timestamp-tooltip';

/**
* Event types whose eventData contains an error field with a StructuredError.
Expand Down Expand Up @@ -211,6 +212,15 @@ function buildDurationMap(events: Event[]): Map<string, DurationInfo> {
return durations;
}

/** Check if a loaded eventData object contains any encrypted marker values. */
function hasEncryptedValues(data: unknown): boolean {
if (!data || typeof data !== 'object') return false;
for (const val of Object.values(data as Record<string, unknown>)) {
if (isEncryptedMarker(val)) return true;
}
return false;
}

function isRunLevel(eventType: string): boolean {
return (
eventType === 'run_created' ||
Expand Down Expand Up @@ -602,21 +612,67 @@ const SORT_OPTIONS = [
function RowsSkeleton() {
return (
<div className="flex-1 overflow-hidden">
{Array.from({ length: 8 }, (_, i) => (
<div
key={i}
className="flex items-center gap-3 px-4"
style={{ height: 40 }}
>
<Skeleton
className="h-2 w-2 flex-shrink-0"
style={{ borderRadius: '50%' }}
/>
<Skeleton className="h-3" style={{ width: 90 }} />
<Skeleton className="h-3" style={{ width: 100 }} />
<Skeleton className="h-3" style={{ width: 80 }} />
<Skeleton className="h-3 flex-1" />
<Skeleton className="h-3 flex-1" />
{Array.from({ length: 16 }, (_, i) => (
<div key={i} className="flex items-center gap-0" style={{ height: 40 }}>
{/* Gutter area */}
<div
className="relative flex-shrink-0 self-stretch flex items-center"
style={{ width: GUTTER_WIDTH }}
>
{/* Vertical line skeleton */}
<div
style={{
position: 'absolute',
left: 8,
top: i === 0 ? '50%' : 0,
bottom: 0,
width: 2,
}}
>
<Skeleton className="w-full h-full" style={{ borderRadius: 1 }} />
</div>
{/* Dot skeleton */}
<Skeleton
className="flex-shrink-0"
style={{
width: i % 4 === 0 ? 8 : 6,
height: i % 4 === 0 ? 8 : 6,
borderRadius: '50%',
marginLeft: i % 4 === 0 ? 5 : 6,
}}
/>
</div>
{/* Chevron placeholder */}
<div className="w-5 flex-shrink-0 flex items-center justify-center">
<Skeleton className="w-5 h-5" style={{ borderRadius: 4 }} />
</div>
{/* Time */}
<div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
<Skeleton className="h-3" style={{ width: '70%' }} />
</div>
{/* Event Type */}
<div
className="min-w-0 px-4 flex items-center gap-1.5"
style={{ flex: '2 1 0%' }}
>
<Skeleton
className="flex-shrink-0"
style={{ width: 6, height: 6, borderRadius: '50%' }}
/>
<Skeleton className="h-3" style={{ width: '60%' }} />
</div>
{/* Name */}
<div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
<Skeleton className="h-3" style={{ width: '50%' }} />
</div>
{/* Correlation ID */}
<div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
<Skeleton className="h-3" style={{ width: '75%' }} />
</div>
{/* Event ID */}
<div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
<Skeleton className="h-3" style={{ width: '75%' }} />
</div>
</div>
))}
</div>
Expand Down Expand Up @@ -668,6 +724,7 @@ function EventRow({
cachedEventData,
onCacheEventData,
encryptionKey,
onEncryptedDataDetected,
}: {
event: Event;
index: number;
Expand All @@ -687,6 +744,7 @@ function EventRow({
cachedEventData: unknown | null;
onCacheEventData: (eventId: string, data: unknown) => void;
encryptionKey?: Uint8Array;
onEncryptedDataDetected?: () => void;
}) {
const [isLoading, setIsLoading] = useState(false);
const [loadedEventData, setLoadedEventData] = useState<unknown | null>(
Expand All @@ -697,6 +755,18 @@ function EventRow({
cachedEventData !== null
);

// Notify parent if cached data has encrypted markers on mount
useEffect(() => {
if (
cachedEventData !== null &&
!encryptionKey &&
hasEncryptedValues(cachedEventData)
) {
onEncryptedDataDetected?.();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const rowGroupKey = isRunLevel(event.eventType)
? '__run__'
: (event.correlationId ?? undefined);
Expand Down Expand Up @@ -745,6 +815,9 @@ function EventRow({
if (data !== null && data !== undefined) {
setLoadedEventData(data);
onCacheEventData(event.eventId, data);
if (!encryptionKey && hasEncryptedValues(data)) {
onEncryptedDataDetected?.();
}
}
} catch (err) {
setLoadError(
Expand All @@ -760,6 +833,8 @@ function EventRow({
hasExistingEventData,
onLoadEventData,
onCacheEventData,
encryptionKey,
onEncryptedDataDetected,
]);

// Auto-load event data when remounting in expanded state without cached data
Expand Down Expand Up @@ -877,7 +952,9 @@ function EventRow({
className="tabular-nums min-w-0 px-4"
style={{ color: 'var(--ds-gray-900)', flex: '2 1 0%' }}
>
{formatEventTime(createdAt)}
<TimestampTooltip date={createdAt}>
<span>{formatEventTime(createdAt)}</span>
</TimestampTooltip>
</div>

{/* Event Type */}
Expand Down Expand Up @@ -1079,22 +1156,26 @@ export function EventListView({
);
}, [events, effectiveSortOrder]);

// Detect encrypted fields across all loaded events.
// Only checks top-level eventData values (input, output, result, etc.) —
// the current data model guarantees encrypted markers appear at this level.
const hasEncryptedData = useMemo(() => {
// Detect encrypted fields across all loaded events (inline eventData).
const hasEncryptedInlineData = useMemo(() => {
if (!events) return false;
for (const event of events) {
const ed = (event as Record<string, unknown>).eventData;
if (!ed || typeof ed !== 'object') continue;
const data = ed as Record<string, unknown>;
for (const val of Object.values(data)) {
if (isEncryptedMarker(val)) return true;
}
if (hasEncryptedValues(ed)) return true;
}
return false;
}, [events]);

// Tracks whether any expanded row's lazy-loaded data contained encrypted markers.
// Set to true by EventRow via onEncryptedDataDetected; never reset (sticky).
const [foundEncryptedInLazyData, setFoundEncryptedInLazyData] =
useState(false);
const handleEncryptedDataDetected = useCallback(() => {
setFoundEncryptedInLazyData(true);
}, []);

const hasEncryptedData = hasEncryptedInlineData || foundEncryptedInLazyData;

const { correlationNameMap, workflowName } = useMemo(
() => buildNameMaps(events ?? null, run ?? null),
[events, run]
Expand Down Expand Up @@ -1268,18 +1349,26 @@ export function EventListView({
</div>
{/* Skeleton header */}
<div
className="flex items-center gap-0 h-10 border-b flex-shrink-0 px-4"
className="flex items-center gap-0 h-10 border-b flex-shrink-0"
style={{ borderColor: 'var(--ds-gray-alpha-200)' }}
>
<Skeleton className="h-3" style={{ width: 60 }} />
<div style={{ flex: 1 }} />
<Skeleton className="h-3" style={{ width: 80 }} />
<div style={{ flex: 1 }} />
<Skeleton className="h-3" style={{ width: 50 }} />
<div style={{ flex: 1 }} />
<Skeleton className="h-3" style={{ width: 90 }} />
<div style={{ flex: 1 }} />
<Skeleton className="h-3" style={{ width: 70 }} />
<div className="flex-shrink-0" style={{ width: GUTTER_WIDTH }} />
<div className="w-5 flex-shrink-0" />
<div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
<Skeleton className="h-3" style={{ width: 40 }} />
</div>
<div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
<Skeleton className="h-3" style={{ width: 72 }} />
</div>
<div className="min-w-0 px-4" style={{ flex: '2 1 0%' }}>
<Skeleton className="h-3" style={{ width: 44 }} />
</div>
<div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
<Skeleton className="h-3" style={{ width: 92 }} />
</div>
<div className="min-w-0 px-4" style={{ flex: '3 1 0%' }}>
<Skeleton className="h-3" style={{ width: 60 }} />
</div>
</div>
<RowsSkeleton />
</div>
Expand Down Expand Up @@ -1455,6 +1544,7 @@ export function EventListView({
}
onCacheEventData={cacheEventData}
encryptionKey={encryptionKey}
onEncryptedDataDetected={handleEncryptedDataDetected}
/>
);
}}
Expand Down
3 changes: 2 additions & 1 deletion packages/web-shared/src/components/hook-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import type { Hook, WorkflowRunStatus } from '@workflow/world';
import { Send } from 'lucide-react';
import { useCallback, useState } from 'react';
import { toast } from 'sonner';
import { useToast } from '../lib/toast';
import { ResolveHookModal } from './sidebar/resolve-hook-modal';

// ============================================================================
Expand Down Expand Up @@ -45,6 +45,7 @@ export function useHookActions({
onResolve,
callbacks,
}: UseHookActionsOptions): UseHookActionsReturn {
const toast = useToast();
const [isResolving, setIsResolving] = useState(false);
const [selectedHook, setSelectedHook] = useState<Hook | null>(null);

Expand Down
31 changes: 21 additions & 10 deletions packages/web-shared/src/components/sidebar/attribute-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import type { ModelMessage } from 'ai';
import { Lock } from 'lucide-react';
import type { KeyboardEvent, ReactNode } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { toast } from 'sonner';
import { useToast } from '../../lib/toast';
import { isEncryptedMarker } from '../../lib/hydration';
import { extractConversation, isDoStreamStep } from '../../lib/utils';
import { StreamClickContext } from '../ui/data-inspector';
import { TimestampTooltip } from '../ui/timestamp-tooltip';
import { ErrorCard } from '../ui/error-card';
import {
ErrorStackBlock,
Expand Down Expand Up @@ -336,6 +337,16 @@ const localMillisecondTimeOrNull = (value: unknown): string | null => {
return formatLocalMillisecondTime(date);
};

const timestampWithTooltipOrNull = (value: unknown): ReactNode | null => {
const date = parseDateValue(value);
if (!date) return null;
return (
<TimestampTooltip date={date}>
<span>{formatLocalMillisecondTime(date)}</span>
</TimestampTooltip>
);
};

interface DisplayContext {
stepName?: string;
}
Expand Down Expand Up @@ -375,15 +386,14 @@ const attributeToDisplayFn: Record<
projectId: (_value: unknown) => null,
environment: (_value: unknown) => null,
executionContext: (_value: unknown) => null,
// Dates
// TODO: relative time with tooltips for ISO times
createdAt: localMillisecondTimeOrNull,
startedAt: localMillisecondTimeOrNull,
updatedAt: localMillisecondTimeOrNull,
completedAt: localMillisecondTimeOrNull,
expiredAt: localMillisecondTimeOrNull,
retryAfter: localMillisecondTimeOrNull,
resumeAt: localMillisecondTimeOrNull,
// Dates — wrapped with TimestampTooltip showing UTC/local + relative time
createdAt: timestampWithTooltipOrNull,
startedAt: timestampWithTooltipOrNull,
updatedAt: timestampWithTooltipOrNull,
completedAt: timestampWithTooltipOrNull,
expiredAt: timestampWithTooltipOrNull,
retryAfter: timestampWithTooltipOrNull,
resumeAt: timestampWithTooltipOrNull,
// Resolved attributes, won't actually use this function
metadata: (value: unknown) => {
if (!hasDisplayContent(value)) return null;
Expand Down Expand Up @@ -683,6 +693,7 @@ export const AttributePanel = ({
/** Callback when a stream reference is clicked */
onStreamClick?: (streamId: string) => void;
}) => {
const toast = useToast();
// Extract workflowCoreVersion from executionContext for display
const displayData = useMemo(() => {
const result = { ...data };
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { Copy } from 'lucide-react';
import { toast } from 'sonner';
import { useToast } from '../../lib/toast';
import { DataInspector } from '../ui/data-inspector';

const serializeForClipboard = (value: unknown): string => {
Expand All @@ -21,6 +21,7 @@ const serializeForClipboard = (value: unknown): string => {
};

export function CopyableDataBlock({ data }: { data: unknown }) {
const toast = useToast();
return (
<div
className="relative overflow-x-auto rounded-md border p-3 pt-9"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
import clsx from 'clsx';
import { Send, Zap } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { toast } from 'sonner';
import { useToast } from '../../lib/toast';
import { isEncryptedMarker } from '../../lib/hydration';
import { DecryptButton } from '../ui/decrypt-button';
import { AttributePanel } from './attribute-panel';
Expand Down Expand Up @@ -104,6 +104,7 @@ export function EntityDetailPanel({
/** Info about the currently selected span from the trace viewer */
selectedSpan: SelectedSpanInfo | null;
}): React.JSX.Element | null {
const toast = useToast();
const [stoppingSleep, setStoppingSleep] = useState(false);
const [showResolveHookModal, setShowResolveHookModal] = useState(false);
const [resolvingHook, setResolvingHook] = useState(false);
Expand Down
4 changes: 4 additions & 0 deletions packages/web-shared/src/components/sidebar/events-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ const DATA_EVENT_TYPES = new Set([
'step_created',
'step_completed',
'step_failed',
'step_retrying',
'hook_created',
'hook_received',
'run_created',
'run_completed',
'run_failed',
'wait_created',
'wait_completed',
]);

/**
Expand Down
Loading
Loading