Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 7 additions & 6 deletions packages/web-shared/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,21 @@ export {
ResolveHookModal,
useHookActions,
} from './hook-actions';
export { TraceViewerSkeleton } from './new-trace-viewer/components/trace-viewer-skeleton';
export { RunTraceView } from './run-trace-view';
export { ConversationView } from './sidebar/conversation-view';
export {
SidebarDataProvider,
type SidebarDataContextValue,
} from './sidebar/sidebar-data-context';
export type {
SelectedSpanInfo,
SpanSelectionInfo,
} from './sidebar/entity-detail-panel';
export {
type SidebarDataContextValue,
SidebarDataProvider,
} from './sidebar/sidebar-data-context';
export type { FetchSpanDetail, SpanDetail } from './sidebar/use-span-detail';
export { type StreamChunk, StreamViewer } from './stream-viewer';
export type { Span, SpanEvent } from './trace-viewer/types';
export { NewTraceViewer } from './trace-viewer-new';
export {
DataInspector,
type DataInspectorProps,
Expand All @@ -37,5 +40,3 @@ export { LoadMoreButton } from './ui/load-more-button';
export { MenuDropdown, type MenuDropdownOption } from './ui/menu-dropdown';
export { Spinner } from './ui/spinner';
export { WorkflowTraceViewer } from './workflow-trace-view';
export { NewTraceViewer } from './trace-viewer-new';
export { TraceViewerSkeleton } from './new-trace-viewer/components/trace-viewer-skeleton';
Original file line number Diff line number Diff line change
Expand Up @@ -734,10 +734,7 @@ function NewTraceViewerContent({
run={sidebar.run}
onStreamClick={sidebar.onStreamClick}
onRunClick={sidebar.onRunClick}
spanDetailData={sidebar.spanDetailData}
spanDetailError={sidebar.spanDetailError}
spanDetailLoading={sidebar.spanDetailLoading}
onSpanSelect={sidebar.onSpanSelect}
fetchSpanDetail={sidebar.fetchSpanDetail}
onWakeUpSleep={sidebar.onWakeUpSleep}
onLoadEventData={sidebar.onLoadEventData}
onResolveHook={sidebar.onResolveHook}
Expand Down
19 changes: 5 additions & 14 deletions packages/web-shared/src/components/run-trace-view.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
'use client';

import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
import type { Event, Hook, WorkflowRun } from '@workflow/world';
import { AlertCircle } from 'lucide-react';
import type { SpanSelectionInfo } from './sidebar/entity-detail-panel';
import type { FetchSpanDetail } from './sidebar/use-span-detail';
import { WorkflowTraceViewer } from './workflow-trace-view';

interface RunTraceViewProps {
run: WorkflowRun;
events: Event[];
isLoading?: boolean;
error?: Error | null;
spanDetailData?: WorkflowRun | Step | Hook | Event | null;
spanDetailLoading?: boolean;
spanDetailError?: Error | null;
fetchSpanDetail?: FetchSpanDetail;
onWakeUpSleep?: (
runId: string,
correlationId: string
Expand All @@ -25,7 +23,6 @@ interface RunTraceViewProps {
onCancelRun?: (runId: string) => Promise<void>;
onStreamClick?: (streamId: string) => void;
onRunClick?: (runId: string) => void;
onSpanSelect?: (info: SpanSelectionInfo) => void;
onLoadMoreSpans?: () => void | Promise<void>;
hasMoreSpans?: boolean;
isLoadingMoreSpans?: boolean;
Expand All @@ -36,15 +33,12 @@ export function RunTraceView({
events,
isLoading,
error,
spanDetailData,
spanDetailLoading,
spanDetailError,
fetchSpanDetail,
onWakeUpSleep,
onResolveHook,
onCancelRun,
onStreamClick,
onRunClick,
onSpanSelect,
onLoadMoreSpans,
hasMoreSpans,
isLoadingMoreSpans,
Expand All @@ -66,15 +60,12 @@ export function RunTraceView({
events={events}
run={run}
isLoading={isLoading}
spanDetailData={spanDetailData}
spanDetailLoading={spanDetailLoading}
spanDetailError={spanDetailError}
fetchSpanDetail={fetchSpanDetail}
onWakeUpSleep={onWakeUpSleep}
onResolveHook={onResolveHook}
onCancelRun={onCancelRun}
onStreamClick={onStreamClick}
onRunClick={onRunClick}
onSpanSelect={onSpanSelect}
onLoadMoreSpans={onLoadMoreSpans}
hasMoreSpans={hasMoreSpans}
isLoadingMoreSpans={isLoadingMoreSpans}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
import clsx from 'clsx';
import { Send, Zap } from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useToast } from '../../lib/toast';
import { DecryptClickContext } from '../ui/data-inspector';
import { AttributePanel } from './attribute-panel';
import { EventsList } from './events-list';
import { ResolveHookModal } from './resolve-hook-modal';
import { useSidebarDataOptional } from './sidebar-data-context';
import { mergeSpanDetail } from './span-detail-merge';
import { type FetchSpanDetail, useSpanDetail } from './use-span-detail';

// Type guards for runtime validation of span attribute data
function isStep(data: unknown): data is Step {
Expand Down Expand Up @@ -57,10 +59,7 @@ export function EntityDetailPanel({
run,
onStreamClick,
onRunClick,
spanDetailData,
spanDetailError,
spanDetailLoading,
onSpanSelect,
fetchSpanDetail,
onWakeUpSleep,
onLoadEventData,
onResolveHook,
Expand All @@ -74,14 +73,12 @@ export function EntityDetailPanel({
onStreamClick?: (streamId: string) => void;
/** Callback when a run reference is clicked */
onRunClick?: (runId: string) => void;
/** Pre-fetched span detail data for the selected span. */
spanDetailData: WorkflowRun | Step | Hook | Event | null;
/** Error from external span detail fetch. */
spanDetailError?: Error | null;
/** Loading state from external span detail fetch. */
spanDetailLoading?: boolean;
/** Callback when a span is selected. Use this to fetch data externally and pass via spanDetailData. */
onSpanSelect: (info: SpanSelectionInfo) => void;
/**
* Fetches the detail (input/output/etc.) for a selected span. The panel
* owns the fetch lifecycle and keys the result to the current selection, so
* the returned data always belongs to the span being rendered.
*/
fetchSpanDetail?: FetchSpanDetail;
/** Callback to wake up a pending sleep call. */
onWakeUpSleep?: (
runId: string,
Expand Down Expand Up @@ -124,53 +121,40 @@ export function EntityDetailPanel({
const rawEvents = selectedSpan?.rawEvents;
const rawEventsLength = rawEvents?.length ?? 0;

// Determine resource type, ID, and runId from the selected span
const { resource, resourceId, runId } = useMemo(() => {
// Determine the selection (resource type, ID, and runId) from the selected span
const selection = useMemo<SpanSelectionInfo | null>(() => {
if (!selectedSpan) {
return { resource: undefined, resourceId: undefined, runId: undefined };
return null;
}

const res = selectedSpan.resource;
if (res === 'step' && isStep(data)) {
return { resource: 'step', resourceId: data.stepId, runId: data.runId };
}
if (res === 'run' && isWorkflowRun(data)) {
return { resource: 'run', resourceId: data.runId, runId: undefined };
return { resource: 'run', resourceId: data.runId };
}
if (res === 'hook' && isHook(data)) {
return { resource: 'hook', resourceId: data.hookId, runId: undefined };
return { resource: 'hook', resourceId: data.hookId };
}
if (res === 'sleep') {
if (res === 'sleep' && selectedSpan.spanId) {
const waitData = data as { runId?: string } | undefined;
return {
resource: 'sleep',
resourceId: selectedSpan.spanId,
runId: waitData?.runId,
};
}
return { resource: undefined, resourceId: undefined, runId: undefined };
return null;
}, [selectedSpan, data]);
const resource = selection?.resource;
const resourceId = selection?.resourceId;

// Notify parent when span selection changes.
// Use a ref for the callback so the effect only fires when the actual
// selection values change, not when the callback identity changes due to
// parent re-renders from polling.
const onSpanSelectRef = useRef(onSpanSelect);
onSpanSelectRef.current = onSpanSelect;

useEffect(() => {
if (
resource &&
resourceId &&
['run', 'step', 'hook', 'sleep'].includes(resource)
) {
onSpanSelectRef.current({
resource: resource as 'run' | 'step' | 'hook' | 'sleep',
resourceId,
runId,
});
}
}, [resource, resourceId, runId]);
const {
data: spanDetailData,
loading: spanDetailLoading,
error: spanDetailError,
} = useSpanDetail(selection, fetchSpanDetail, { encryptionKey });

// Check if this sleep is still pending and can be woken up
const canWakeUp = useMemo(() => {
Expand Down Expand Up @@ -213,7 +197,7 @@ export function EntityDetailPanel({
]);

const error = spanDetailError ?? undefined;
const loading = spanDetailLoading ?? false;
const loading = spanDetailLoading;

// Get the hook token for resolving (prefer fetched data, then hooks array fallback)
const hookToken = useMemo(() => {
Expand Down Expand Up @@ -313,14 +297,15 @@ export function EntityDetailPanel({
[onResolveHook, hookToken, resolvingHook, spanDetailData, data]
);

// Prefer externally-fetched details when available. For sleep spans, the
// host fetches full correlated events (withData=true) and materializes a wait
// entity, so this includes resumeAt/completedAt without bloating trace payloads.
const displayData = (spanDetailData ?? data) as
| WorkflowRun
| Step
| Hook
| Event;
const displayData = useMemo(
() =>
mergeSpanDetail(data, spanDetailData) as
| WorkflowRun
| Step
| Hook
| Event,
[data, spanDetailData]
);

const moduleSpecifier = useMemo(() => {
const displayRecord = displayData as Record<string, unknown>;
Expand Down Expand Up @@ -435,6 +420,8 @@ export function EntityDetailPanel({
<EventsList
events={rawEvents}
onLoadEventData={onLoadEventData}
onStreamClick={onStreamClick}
onRunClick={onRunClick}
encryptionKey={encryptionKey}
/>
)}
Expand Down
59 changes: 34 additions & 25 deletions packages/web-shared/src/components/sidebar/events-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { EVENT_DATA_REF_FIELDS, type Event } from '@workflow/world';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { hasEncryptedFields, isExpiredMarker } from '../../lib/hydration';
import { RunClickContext, StreamClickContext } from '../ui/data-inspector';
import { ErrorCard } from '../ui/error-card';
import { ErrorStackBlock, isStructuredError } from '../ui/error-stack-block';
import { Skeleton } from '../ui/skeleton';
Expand Down Expand Up @@ -256,6 +257,8 @@ export function EventsList({
isLoading = false,
error,
onLoadEventData,
onStreamClick,
onRunClick,
encryptionKey,
}: {
events: Event[];
Expand All @@ -265,6 +268,8 @@ export function EventsList({
correlationId: string,
eventId: string
) => Promise<unknown | null>;
onStreamClick?: (streamId: string) => void;
onRunClick?: (runId: string) => void;
/** When provided, signals that decryption is active (triggers re-load of expanded events) */
encryptionKey?: Uint8Array;
}) {
Expand All @@ -285,31 +290,35 @@ export function EventsList({
}

return (
<DetailCard summary="Events" contentClassName="mb-0" defaultOpen>
{isLoading ? (
<div className="flex flex-col -mx-4">
{[0, 1, 2].map((i) => (
<div
key={i}
className="flex items-center justify-between gap-3 bg-background-200 px-4 py-2"
>
<Skeleton className="h-4 w-32 rounded" />
<Skeleton className="h-3 w-16 rounded" />
<RunClickContext.Provider value={onRunClick}>
<StreamClickContext.Provider value={onStreamClick}>
<DetailCard summary="Events" contentClassName="mb-0" defaultOpen>
{isLoading ? (
<div className="flex flex-col -mx-4">
{[0, 1, 2].map((i) => (
<div
key={i}
className="flex items-center justify-between gap-3 bg-background-200 px-4 py-2"
>
<Skeleton className="h-4 w-32 rounded" />
<Skeleton className="h-3 w-16 rounded" />
</div>
))}
</div>
))}
</div>
) : (
<div className="flex flex-col -mx-4">
{sortedEvents.map((event) => (
<EventItem
key={event.eventId}
event={event}
onLoadEventData={onLoadEventData}
encryptionKey={encryptionKey}
/>
))}
</div>
)}
</DetailCard>
) : (
<div className="flex flex-col -mx-4">
{sortedEvents.map((event) => (
<EventItem
key={event.eventId}
event={event}
onLoadEventData={onLoadEventData}
encryptionKey={encryptionKey}
/>
))}
</div>
)}
</DetailCard>
</StreamClickContext.Provider>
</RunClickContext.Provider>
);
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
'use client';

import { createContext, useContext, type ReactNode } from 'react';
import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
import type { SpanSelectionInfo } from './entity-detail-panel';
import type { Event, Hook, WorkflowRun } from '@workflow/world';
import { createContext, type ReactNode, useContext } from 'react';
import type { FetchSpanDetail } from './use-span-detail';

export interface SidebarDataContextValue {
run: WorkflowRun;
events: Event[];
spanDetailData: WorkflowRun | Step | Hook | Event | null;
spanDetailError?: Error | null;
spanDetailLoading?: boolean;
onSpanSelect: (info: SpanSelectionInfo) => void;
/** Fetches the detail for a selected span; the panel owns the lifecycle. */
fetchSpanDetail?: FetchSpanDetail;
onStreamClick?: (streamId: string) => void;
onRunClick?: (runId: string) => void;
onWakeUpSleep?: (
Expand Down
Loading
Loading