@@ -19,26 +19,26 @@ import {
1919 Search as SearchIcon ,
2020 Tooltip ,
2121} from '@/components/emcn'
22- import { AgentSkillsIcon , WorkflowIcon } from '@/components/icons'
2322import { cn } from '@/lib/core/utils/cn'
2423import type { TraceSpan } from '@/lib/logs/types'
25- import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config'
26- import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config'
27- import { getBlock , getBlockByToolName } from '@/blocks'
24+ import {
25+ formatCostAmount ,
26+ formatTokensSummary ,
27+ formatTps ,
28+ formatTtft ,
29+ getBlockIconAndColor ,
30+ getDisplayName ,
31+ hasErrorInTree ,
32+ hasUnhandledErrorInTree ,
33+ isIterationType ,
34+ parseTime ,
35+ } from '@/app/workspace/[workspaceId]/logs/components/log-details/utils'
2836import { useCodeViewerFeatures } from '@/hooks/use-code-viewer'
2937
3038interface TraceSpansProps {
3139 traceSpans ?: TraceSpan [ ]
3240}
3341
34- /**
35- * Checks if a span type is a loop or parallel iteration
36- */
37- function isIterationType ( type : string ) : boolean {
38- const lower = type ?. toLowerCase ( ) || ''
39- return lower === 'loop-iteration' || lower === 'parallel-iteration'
40- }
41-
4242/**
4343 * Creates a toggle handler for Set-based state
4444 */
@@ -59,124 +59,9 @@ function useSetToggle() {
5959 )
6060}
6161
62- /**
63- * Formats a token count with locale-aware thousands separators.
64- * Returns `undefined` for missing or non-positive counts so callers can
65- * filter them out before rendering.
66- */
67- function formatTokenCount ( value : number | undefined ) : string | undefined {
68- if ( typeof value !== 'number' || ! Number . isFinite ( value ) || value <= 0 ) return undefined
69- return value . toLocaleString ( 'en-US' )
70- }
71-
72- /**
73- * Builds a compact, dot-separated token summary for a span:
74- * `"1,234 in · 567 out · 1,801 total"` with cache/reasoning appended when
75- * present. Returns `undefined` when the span has no meaningful token data.
76- */
77- function formatTokensSummary ( tokens : TraceSpan [ 'tokens' ] ) : string | undefined {
78- if ( ! tokens ) return undefined
79- const parts : string [ ] = [ ]
80- const input = formatTokenCount ( tokens . input )
81- const output = formatTokenCount ( tokens . output )
82- const total = formatTokenCount ( tokens . total )
83- const cacheRead = formatTokenCount ( tokens . cacheRead )
84- const cacheWrite = formatTokenCount ( tokens . cacheWrite )
85- const reasoning = formatTokenCount ( tokens . reasoning )
86- if ( input ) parts . push ( `${ input } in` )
87- if ( cacheRead ) parts . push ( `${ cacheRead } cached` )
88- if ( cacheWrite ) parts . push ( `${ cacheWrite } cache write` )
89- if ( output ) parts . push ( `${ output } out` )
90- if ( reasoning ) parts . push ( `${ reasoning } reasoning` )
91- if ( total ) parts . push ( `${ total } total` )
92- return parts . length > 0 ? parts . join ( ' · ' ) : undefined
93- }
94-
95- /**
96- * Formats a USD cost value for display. Shows `<$0.0001` for non-zero sub-cent
97- * amounts so the user sees it was counted.
98- */
99- function formatCostAmount ( value : number | undefined ) : string | undefined {
100- if ( typeof value !== 'number' || ! Number . isFinite ( value ) || value <= 0 ) return undefined
101- if ( value < 0.0001 ) return '<$0.0001'
102- return `$${ value . toFixed ( 4 ) } `
103- }
104-
105- /**
106- * Builds a compact cost summary: `"$0.0023 · $0.0001 in · $0.0022 out"`.
107- * Falls back to whichever parts are present.
108- */
10962function formatCostSummary ( cost : TraceSpan [ 'cost' ] ) : string | undefined {
11063 if ( ! cost ) return undefined
111- const parts : string [ ] = [ ]
112- const total = formatCostAmount ( cost . total )
113- const input = formatCostAmount ( cost . input )
114- const output = formatCostAmount ( cost . output )
115- if ( total ) parts . push ( total )
116- if ( input ) parts . push ( `${ input } in` )
117- if ( output ) parts . push ( `${ output } out` )
118- return parts . length > 0 ? parts . join ( ' · ' ) : undefined
119- }
120-
121- /**
122- * Derives tokens-per-second from output tokens over segment duration.
123- * Returns `undefined` when inputs are missing or non-positive.
124- */
125- function formatTps ( outputTokens : number | undefined , durationMs : number ) : string | undefined {
126- if ( typeof outputTokens !== 'number' || ! ( outputTokens > 0 ) ) return undefined
127- if ( ! ( durationMs > 0 ) ) return undefined
128- const tps = Math . round ( outputTokens / ( durationMs / 1000 ) )
129- if ( ! ( tps > 0 ) ) return undefined
130- return `${ tps . toLocaleString ( 'en-US' ) } tok/s`
131- }
132-
133- /**
134- * Formats time-to-first-token. Uses `ms` below 1000, `s` above.
135- */
136- function formatTtft ( ms : number | undefined ) : string | undefined {
137- if ( typeof ms !== 'number' || ! Number . isFinite ( ms ) || ms < 0 ) return undefined
138- if ( ms < 1000 ) return `${ Math . round ( ms ) } ms`
139- return `${ ( ms / 1000 ) . toFixed ( 2 ) } s`
140- }
141-
142- /**
143- * Parses a time value to milliseconds
144- */
145- function parseTime ( value ?: string | number | null ) : number {
146- if ( ! value ) return 0
147- const ms = typeof value === 'number' ? value : new Date ( value ) . getTime ( )
148- return Number . isFinite ( ms ) ? ms : 0
149- }
150-
151- /**
152- * Checks if a span or any of its descendants has an error (any error).
153- */
154- function hasErrorInTree ( span : TraceSpan ) : boolean {
155- if ( span . status === 'error' ) return true
156- if ( span . children && span . children . length > 0 ) {
157- return span . children . some ( ( child ) => hasErrorInTree ( child ) )
158- }
159- if ( span . toolCalls && span . toolCalls . length > 0 ) {
160- return span . toolCalls . some ( ( tc ) => tc . error )
161- }
162- return false
163- }
164-
165- /**
166- * Checks if a span or any of its descendants has an unhandled error.
167- * Spans with errorHandled: true (including containers that propagate it)
168- * are skipped. Used only for the root workflow span to match the actual
169- * workflow status.
170- */
171- function hasUnhandledErrorInTree ( span : TraceSpan ) : boolean {
172- if ( span . status === 'error' && ! span . errorHandled ) return true
173- if ( span . children && span . children . length > 0 ) {
174- return span . children . some ( ( child ) => hasUnhandledErrorInTree ( child ) )
175- }
176- if ( span . toolCalls && span . toolCalls . length > 0 && ! span . errorHandled ) {
177- return span . toolCalls . some ( ( tc ) => tc . error )
178- }
179- return false
64+ return formatCostAmount ( cost . total )
18065}
18166
18267/**
@@ -201,53 +86,6 @@ function normalizeAndSortSpans(spans: TraceSpan[]): TraceSpan[] {
20186 } )
20287}
20388
204- const DEFAULT_BLOCK_COLOR = '#6b7280'
205-
206- /**
207- * Gets icon and color for a span type using block config
208- */
209- function getBlockIconAndColor (
210- type : string ,
211- toolName ?: string
212- ) : {
213- icon : React . ComponentType < { className ?: string } > | null
214- bgColor : string
215- } {
216- const lowerType = type . toLowerCase ( )
217-
218- // Check for tool by name first (most specific)
219- if ( lowerType === 'tool' && toolName ) {
220- // Handle load_skill tool with the AgentSkillsIcon
221- if ( toolName === 'load_skill' ) {
222- return { icon : AgentSkillsIcon , bgColor : '#8B5CF6' }
223- }
224- const toolBlock = getBlockByToolName ( toolName )
225- if ( toolBlock ) {
226- return { icon : toolBlock . icon , bgColor : toolBlock . bgColor }
227- }
228- }
229-
230- // Special types not in block registry
231- if ( lowerType === 'loop' || lowerType === 'loop-iteration' ) {
232- return { icon : LoopTool . icon , bgColor : LoopTool . bgColor }
233- }
234- if ( lowerType === 'parallel' || lowerType === 'parallel-iteration' ) {
235- return { icon : ParallelTool . icon , bgColor : ParallelTool . bgColor }
236- }
237- if ( lowerType === 'workflow' ) {
238- return { icon : WorkflowIcon , bgColor : '#6366F1' }
239- }
240-
241- // Look up from block registry (model maps to agent)
242- const blockType = lowerType === 'model' ? 'agent' : lowerType
243- const blockConfig = getBlock ( blockType )
244- if ( blockConfig ) {
245- return { icon : blockConfig . icon , bgColor : blockConfig . bgColor }
246- }
247-
248- return { icon : null , bgColor : DEFAULT_BLOCK_COLOR }
249- }
250-
25189/**
25290 * Renders the progress bar showing execution timeline
25391 */
@@ -403,10 +241,10 @@ function InputOutputSection({
403241 { label }
404242 </ span >
405243 < ChevronDown
406- className = 'h-[8px] w-[8px] text-[var(--text-tertiary)] transition-colors transition-transform group-hover:text-[var(--text-primary)]'
407- style = { {
408- transform : isExpanded ? 'rotate(180deg)' : 'rotate(0deg)' ,
409- } }
244+ className = { cn (
245+ 'h-[8px] w-[8px] text-[var(--text-tertiary)] transition-colors transition-transform duration-100 group-hover:text-[var(--text-primary)]' ,
246+ isExpanded && 'rotate-180'
247+ ) }
410248 />
411249 </ div >
412250 { isExpanded && (
@@ -684,7 +522,7 @@ const TraceSpanNode = memo(function TraceSpanNode({
684522 className = 'min-w-0 max-w-[180px] truncate font-medium text-caption'
685523 style = { { color : showErrorStyle ? 'var(--text-error)' : 'var(--text-secondary)' } }
686524 >
687- { span . name }
525+ { getDisplayName ( span ) }
688526 </ span >
689527 { isToggleable && (
690528 < ChevronDown
0 commit comments