Skip to content

Commit a9d4e2e

Browse files
authored
fix(trace): normalize keyed tool names and show credits in trace view (#4344)
* fix(trace): normalize keyed tool names and show credits in trace view - Export normalizeToolId from tools/index.ts so trace-view can reuse it - Strip resource-id suffixes (knowledge_search_<uuid>, workflow_executor_<uuid>, table_*_<id>) from tool span names at display time so icons resolve and names are readable - Replace raw dollar formatting with credits in trace header and agent detail panel * fix(trace): fix sub-credit display, locale, and pluralization in formatCostAmount * fix(trace): extract normalizeToolId to client-safe module, consolidate log-details formatting utils * refactor(trace): consolidate shared span utilities into log-details/utils * fix(tools): import normalizeToolId for internal use after extracting to normalize.ts * style(trace-spans): replace inline transform style with Tailwind rotate-180 on disclosure chevron * fix(trace-spans): show credits instead of dollars for cost display * refactor(trace): move formatCostAmount and getDisplayName to shared utils, normalize tool names in trace-spans * fix(trace-spans): simplify formatCostSummary to show total credits only
1 parent 1a321c5 commit a9d4e2e

5 files changed

Lines changed: 198 additions & 323 deletions

File tree

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

Lines changed: 18 additions & 180 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,26 @@ import {
1919
Search as SearchIcon,
2020
Tooltip,
2121
} from '@/components/emcn'
22-
import { AgentSkillsIcon, WorkflowIcon } from '@/components/icons'
2322
import { cn } from '@/lib/core/utils/cn'
2423
import 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'
2836
import { useCodeViewerFeatures } from '@/hooks/use-code-viewer'
2937

3038
interface 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-
*/
10962
function 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

Comments
 (0)