Skip to content

Commit 95df032

Browse files
waleedlatif1claude
andcommitted
feat(logs): retry failed runs + show workflow input in detail
Brings PR #4181 inline: persists workflowInput on successful runs, adds useRetryExecution mutation (streaming read-one-chunk-and-cancel), Retry entrypoints in the row context menu and the detail sidebar, and extractRetryInput with fallback to starter block state for older logs. Also surfaces the captured input in a new "Workflow Input" section above Workflow Output in the detail Overview tab, guarded so older logs without the field don't render an empty block. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 7077b49 commit 95df032

File tree

8 files changed

+327
-88
lines changed

8 files changed

+327
-88
lines changed

apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx

Lines changed: 151 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ import { getBlock, getBlockByToolName } from '@/blocks'
3636
import { useCodeViewerFeatures } from '@/hooks/use-code-viewer'
3737

3838
const DEFAULT_BLOCK_COLOR = '#6b7280'
39-
const TREE_PANE_WIDTH = 240
39+
const TREE_PANE_WIDTH = 300
4040
const INDENT_PX = 12
41+
const MIN_BAR_PCT = 0.5
4142

4243
interface TraceViewProps {
4344
traceSpans: TraceSpan[]
@@ -47,6 +48,7 @@ interface FlatSpanEntry {
4748
span: TraceSpan
4849
depth: number
4950
parentIds: string[]
51+
parentDuration?: number
5052
}
5153

5254
interface BlockAppearance {
@@ -186,21 +188,28 @@ function formatTps(outputTokens: number | undefined, durationMs: number): string
186188

187189
/**
188190
* Flattens the visible (expanded) span tree into a linear list for keyboard
189-
* navigation, carrying depth and the chain of parent ids for indent drawing.
191+
* navigation, carrying depth, the chain of parent ids for indent drawing, and
192+
* the immediate parent's duration for percentage-of-parent calculations.
190193
*/
191194
function flattenVisible(spans: TraceSpan[], expanded: Set<string>): FlatSpanEntry[] {
192195
const out: FlatSpanEntry[] = []
193-
const walk = (list: TraceSpan[], depth: number, parents: string[]) => {
196+
const walk = (
197+
list: TraceSpan[],
198+
depth: number,
199+
parents: string[],
200+
parentDuration: number | undefined
201+
) => {
194202
for (const span of list) {
195203
const id = getSpanId(span)
196-
out.push({ span, depth, parentIds: parents })
204+
out.push({ span, depth, parentIds: parents, parentDuration })
197205
const children = getDisplayChildren(span)
198206
if (children.length > 0 && expanded.has(id)) {
199-
walk(children, depth + 1, [...parents, id])
207+
const ownDuration = span.duration || parseTime(span.endTime) - parseTime(span.startTime)
208+
walk(children, depth + 1, [...parents, id], ownDuration)
200209
}
201210
}
202211
}
203-
walk(spans, 0, [])
212+
walk(spans, 0, [], undefined)
204213
return out
205214
}
206215

@@ -270,8 +279,10 @@ function collectMatchingIds(spans: TraceSpan[], query: string): Set<string> {
270279
}
271280

272281
/**
273-
* Row in the tree pane. Renders the span icon, name, duration, and indentation
274-
* guides. Clicking selects the span; the chevron toggles expansion.
282+
* Row in the tree pane. Renders the span icon, name, duration, a hover tooltip
283+
* with timing context, and a Gantt-style mini timeline bar below the row so the
284+
* span's position within the run is visible at a glance. Clicking selects the
285+
* span; the chevron toggles expansion.
275286
*/
276287
const TraceTreeRow = memo(function TraceTreeRow({
277288
entry,
@@ -281,6 +292,8 @@ const TraceTreeRow = memo(function TraceTreeRow({
281292
onSelect,
282293
onToggleExpand,
283294
matchQuery,
295+
runStartMs,
296+
runTotalMs,
284297
}: {
285298
entry: FlatSpanEntry
286299
isSelected: boolean
@@ -289,22 +302,33 @@ const TraceTreeRow = memo(function TraceTreeRow({
289302
onSelect: (id: string) => void
290303
onToggleExpand: (id: string) => void
291304
matchQuery: string
305+
runStartMs: number
306+
runTotalMs: number
292307
}) {
293-
const { span, depth } = entry
308+
const { span, depth, parentDuration } = entry
294309
const id = getSpanId(span)
295-
const duration = span.duration || parseTime(span.endTime) - parseTime(span.startTime)
310+
const startMs = parseTime(span.startTime)
311+
const endMs = parseTime(span.endTime)
312+
const duration = span.duration || endMs - startMs
296313
const isRootWorkflow = depth === 0 && span.type?.toLowerCase() === 'workflow'
297314
const hasError = isRootWorkflow ? hasUnhandledErrorInTree(span) : hasErrorInTree(span)
298315
const { icon: BlockIcon, bgColor } = getBlockAppearance(span.type, span.name)
299316
const nameMatches = !!matchQuery && spanMatchesQuery(span, matchQuery)
300317

318+
const offsetMs = runStartMs > 0 ? Math.max(0, startMs - runStartMs) : 0
319+
const offsetPct = runTotalMs > 0 ? Math.min(100, (offsetMs / runTotalMs) * 100) : 0
320+
const rawDurationPct = runTotalMs > 0 ? (duration / runTotalMs) * 100 : 0
321+
const durationPct = Math.max(MIN_BAR_PCT, Math.min(100 - offsetPct, rawDurationPct))
322+
const pctOfTotal = runTotalMs > 0 ? (duration / runTotalMs) * 100 : null
323+
const pctOfParent =
324+
parentDuration && parentDuration > 0 ? (duration / parentDuration) * 100 : null
325+
301326
return (
302327
<div
303328
className={cn(
304-
'group relative flex min-w-0 cursor-pointer items-center gap-1.5 py-1 pr-2 transition-colors',
329+
'group relative flex min-w-0 cursor-pointer flex-col transition-colors',
305330
isSelected ? 'bg-[var(--surface-3)]' : 'hover-hover:bg-[var(--surface-2)]'
306331
)}
307-
style={{ paddingLeft: 8 + depth * INDENT_PX }}
308332
onClick={() => onSelect(id)}
309333
onKeyDown={(e) => {
310334
if (e.key === 'Enter' || e.key === ' ') {
@@ -318,44 +342,84 @@ const TraceTreeRow = memo(function TraceTreeRow({
318342
aria-expanded={canExpand ? isExpanded : undefined}
319343
aria-level={depth + 1}
320344
>
321-
{canExpand ? (
322-
<button
323-
type='button'
324-
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded-sm text-[var(--text-tertiary)] transition-colors hover-hover:bg-[var(--surface-4)] hover-hover:text-[var(--text-primary)]'
325-
onClick={(e) => {
326-
e.stopPropagation()
327-
onToggleExpand(id)
328-
}}
329-
aria-label={isExpanded ? 'Collapse' : 'Expand'}
330-
>
331-
<ChevronDown
332-
className='h-[10px] w-[10px]'
333-
style={{ transform: isExpanded ? 'rotate(0deg)' : 'rotate(-90deg)' }}
345+
<div
346+
className='flex min-w-0 items-center gap-1.5 pt-1 pr-2'
347+
style={{ paddingLeft: 8 + depth * INDENT_PX }}
348+
>
349+
{canExpand ? (
350+
<button
351+
type='button'
352+
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded-sm text-[var(--text-tertiary)] transition-colors hover-hover:bg-[var(--surface-4)] hover-hover:text-[var(--text-primary)]'
353+
onClick={(e) => {
354+
e.stopPropagation()
355+
onToggleExpand(id)
356+
}}
357+
aria-label={isExpanded ? 'Collapse' : 'Expand'}
358+
>
359+
<ChevronDown
360+
className={cn(
361+
'h-[10px] w-[10px] transition-transform duration-100',
362+
!isExpanded && '-rotate-90'
363+
)}
364+
/>
365+
</button>
366+
) : (
367+
<div className='h-[14px] w-[14px] flex-shrink-0' />
368+
)}
369+
{!isIterationType(span.type) && (
370+
<div
371+
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center overflow-hidden rounded-sm'
372+
style={{ background: bgColor }}
373+
>
374+
{BlockIcon && <BlockIcon className='h-[9px] w-[9px] text-white' />}
375+
</div>
376+
)}
377+
<Tooltip.Root>
378+
<Tooltip.Trigger asChild>
379+
<span
380+
className={cn(
381+
'min-w-0 flex-1 truncate font-medium text-caption',
382+
hasError ? 'text-[var(--text-error)]' : 'text-[var(--text-secondary)]',
383+
nameMatches && 'text-[var(--text-primary)]'
384+
)}
385+
>
386+
{span.name}
387+
</span>
388+
</Tooltip.Trigger>
389+
<Tooltip.Content side='right' className='max-w-[320px]'>
390+
<div className='flex flex-col gap-0.5'>
391+
<span className='font-medium'>{span.name}</span>
392+
<span className='text-[var(--text-tertiary)] text-caption'>
393+
{formatDuration(duration, { precision: 2 }) || '—'}
394+
{offsetMs > 0 && ` · +${formatDuration(offsetMs, { precision: 2 })}`}
395+
</span>
396+
{pctOfTotal !== null && pctOfTotal >= 0.1 && (
397+
<span className='text-[var(--text-tertiary)] text-caption'>
398+
{pctOfTotal.toFixed(pctOfTotal >= 10 ? 0 : 1)}% of total
399+
{pctOfParent !== null &&
400+
pctOfParent >= 0.1 &&
401+
` · ${pctOfParent.toFixed(pctOfParent >= 10 ? 0 : 1)}% of parent`}
402+
</span>
403+
)}
404+
</div>
405+
</Tooltip.Content>
406+
</Tooltip.Root>
407+
<span className='flex-shrink-0 font-medium text-[var(--text-tertiary)] text-caption tabular-nums'>
408+
{formatDuration(duration, { precision: 2 })}
409+
</span>
410+
</div>
411+
<div className='pt-[3px] pr-2 pb-[5px] pl-2'>
412+
<div className='relative h-[3px] w-full overflow-hidden rounded-full bg-[var(--border)]'>
413+
<div
414+
className='absolute h-full rounded-full'
415+
style={{
416+
left: `${offsetPct}%`,
417+
width: `${durationPct}%`,
418+
backgroundColor: hasError ? 'var(--text-error)' : bgColor,
419+
}}
334420
/>
335-
</button>
336-
) : (
337-
<div className='h-[14px] w-[14px] flex-shrink-0' />
338-
)}
339-
{!isIterationType(span.type) && (
340-
<div
341-
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center overflow-hidden rounded-sm'
342-
style={{ background: bgColor }}
343-
>
344-
{BlockIcon && <BlockIcon className='h-[9px] w-[9px] text-white' />}
345421
</div>
346-
)}
347-
<span
348-
className={cn(
349-
'min-w-0 flex-1 truncate font-medium text-caption',
350-
hasError ? 'text-[var(--text-error)]' : 'text-[var(--text-secondary)]',
351-
nameMatches && 'text-[var(--text-primary)]'
352-
)}
353-
>
354-
{span.name}
355-
</span>
356-
<span className='flex-shrink-0 font-medium text-[var(--text-tertiary)] text-caption'>
357-
{formatDuration(duration, { precision: 2 })}
358-
</span>
422+
</div>
359423
</div>
360424
)
361425
})
@@ -448,8 +512,10 @@ function DetailCodeSection({
448512
{label}
449513
</span>
450514
<ChevronDown
451-
className='h-[8px] w-[8px] text-[var(--text-tertiary)] transition-colors transition-transform group-hover:text-[var(--text-primary)]'
452-
style={{ transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}
515+
className={cn(
516+
'h-[8px] w-[8px] text-[var(--text-tertiary)] transition-colors transition-transform duration-100 group-hover:text-[var(--text-primary)]',
517+
!isOpen && '-rotate-90'
518+
)}
453519
/>
454520
</div>
455521
{isOpen && (
@@ -458,7 +524,7 @@ function DetailCodeSection({
458524
<Code.Viewer
459525
code={jsonString}
460526
language='json'
461-
className='!bg-[var(--surface-4)] dark:!bg-[var(--surface-3)] max-h-[360px] min-h-0 max-w-full rounded-md border-0 [word-break:break-all]'
527+
className='!bg-[var(--surface-4)] dark:!bg-[var(--surface-3)] max-w-full rounded-md border-0 [word-break:break-all]'
462528
wrapText
463529
searchQuery={isSearchActive ? searchQuery : undefined}
464530
currentMatchIndex={currentMatchIndex}
@@ -759,36 +825,39 @@ const TraceDetailPane = memo(function TraceDetailPane({ span }: { span: TraceSpa
759825
export const TraceView = memo(function TraceView({ traceSpans }: TraceViewProps) {
760826
const containerRef = useRef<HTMLDivElement>(null)
761827
const [searchQuery, setSearchQuery] = useState('')
762-
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(() => new Set())
763-
const [hasInitialized, setHasInitialized] = useState(false)
764-
const [selectedId, setSelectedId] = useState<string | null>(null)
765-
766-
const { normalizedSpans, allIds, totalDuration, firstRootId, blockCount } = useMemo(() => {
767-
const sorted = normalizeAndSort(traceSpans ?? [])
768-
let earliest = Number.POSITIVE_INFINITY
769-
let latest = 0
770-
for (const span of sorted) {
771-
const s = parseTime(span.startTime)
772-
const e = parseTime(span.endTime)
773-
if (s < earliest) earliest = s
774-
if (e > latest) latest = e
775-
}
776-
const ids = collectAllIds(sorted)
777-
const count = ids.length
778-
return {
779-
normalizedSpans: sorted,
780-
allIds: ids,
781-
totalDuration: latest > earliest ? latest - earliest : 0,
782-
firstRootId: sorted.length > 0 ? getSpanId(sorted[0]) : null,
783-
blockCount: count,
784-
}
785-
}, [traceSpans])
786828

787-
useEffect(() => {
829+
const { normalizedSpans, allIds, totalDuration, runStartMs, firstRootId, blockCount } =
830+
useMemo(() => {
831+
const sorted = normalizeAndSort(traceSpans ?? [])
832+
let earliest = Number.POSITIVE_INFINITY
833+
let latest = 0
834+
for (const span of sorted) {
835+
const s = parseTime(span.startTime)
836+
const e = parseTime(span.endTime)
837+
if (s < earliest) earliest = s
838+
if (e > latest) latest = e
839+
}
840+
const ids = collectAllIds(sorted)
841+
const count = ids.length
842+
const runStart = earliest !== Number.POSITIVE_INFINITY ? earliest : 0
843+
return {
844+
normalizedSpans: sorted,
845+
allIds: ids,
846+
totalDuration: latest > runStart ? latest - runStart : 0,
847+
runStartMs: runStart,
848+
firstRootId: sorted.length > 0 ? getSpanId(sorted[0]) : null,
849+
blockCount: count,
850+
}
851+
}, [traceSpans])
852+
853+
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(() => new Set(allIds))
854+
const [selectedId, setSelectedId] = useState<string | null>(firstRootId)
855+
const [prevAllIds, setPrevAllIds] = useState(allIds)
856+
if (prevAllIds !== allIds) {
857+
setPrevAllIds(allIds)
788858
setExpandedNodes(new Set(allIds))
789859
setSelectedId(firstRootId)
790-
setHasInitialized(true)
791-
}, [allIds, firstRootId])
860+
}
792861

793862
const matchingIds = useMemo(
794863
() => (searchQuery ? collectMatchingIds(normalizedSpans, searchQuery) : null),
@@ -894,7 +963,7 @@ export const TraceView = memo(function TraceView({ traceSpans }: TraceViewProps)
894963
>
895964
{runStatus === 'error' ? 'Error' : 'Success'}
896965
</span>
897-
<span className='flex-shrink-0 font-medium text-[var(--text-secondary)] text-caption'>
966+
<span className='flex-shrink-0 font-medium text-[var(--text-secondary)] text-caption tabular-nums'>
898967
{formatDuration(totalDuration, { precision: 2 }) || '—'}
899968
</span>
900969
<span className='flex-shrink-0 font-medium text-[var(--text-tertiary)] text-caption'>
@@ -949,7 +1018,7 @@ export const TraceView = memo(function TraceView({ traceSpans }: TraceViewProps)
9491018
style={{ width: TREE_PANE_WIDTH }}
9501019
role='tree'
9511020
>
952-
{hasInitialized && flatList.length === 0 && (
1021+
{flatList.length === 0 && (
9531022
<div className='p-3 text-[var(--text-tertiary)] text-caption'>No matching spans</div>
9541023
)}
9551024
{flatList.map((entry) => {
@@ -965,6 +1034,8 @@ export const TraceView = memo(function TraceView({ traceSpans }: TraceViewProps)
9651034
onSelect={handleSelect}
9661035
onToggleExpand={handleToggleExpand}
9671036
matchQuery={searchQuery}
1037+
runStartMs={runStartMs}
1038+
runTotalMs={totalDuration}
9681039
/>
9691040
)
9701041
})}

0 commit comments

Comments
 (0)