From 6be088d1e1b4acc4df8b39b3a329f0c11122955f Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 29 Apr 2026 13:28:18 -0700 Subject: [PATCH 1/9] 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_, workflow_executor_, table_*_) 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 --- .../components/trace-view/trace-view.tsx | 25 +++++++++++++------ apps/sim/tools/index.ts | 2 +- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx index 2fdca2d78e9..09317d49bd6 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx @@ -30,6 +30,7 @@ import { Tooltip, } from '@/components/emcn' import { AgentSkillsIcon, WorkflowIcon } from '@/components/icons' +import { dollarsToCredits } from '@/lib/billing/credits/conversion' import { cn } from '@/lib/core/utils/cn' import type { TraceSpan } from '@/lib/logs/types' import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config' @@ -37,6 +38,7 @@ import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/compo import { getBlock, getBlockByToolName } from '@/blocks' import { useCodeViewerFeatures } from '@/hooks/use-code-viewer' import { PROVIDER_DEFINITIONS } from '@/providers/models' +import { normalizeToolId } from '@/tools' const DEFAULT_BLOCK_COLOR = '#6b7280' const DEFAULT_TREE_PANE_WIDTH = 360 @@ -153,8 +155,9 @@ function getDisplayChildren(span: TraceSpan): TraceSpan[] { function getBlockAppearance(type: string, toolName?: string, provider?: string): BlockAppearance { const lowerType = type.toLowerCase() if (lowerType === 'tool' && toolName) { - if (toolName === 'load_skill') return { icon: AgentSkillsIcon, bgColor: '#8B5CF6' } - const toolBlock = getBlockByToolName(toolName) + const normalized = normalizeToolId(toolName) + if (normalized === 'load_skill') return { icon: AgentSkillsIcon, bgColor: '#8B5CF6' } + const toolBlock = getBlockByToolName(normalized) if (toolBlock) return { icon: toolBlock.icon, bgColor: toolBlock.bgColor } } if (lowerType === 'loop' || lowerType === 'loop-iteration') @@ -191,8 +194,9 @@ function formatTokenCount(value: number | undefined): string | undefined { function formatCostAmount(value: number | undefined): string | undefined { if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined - if (value < 0.0001) return '<$0.0001' - return `$${value.toFixed(4)}` + const credits = dollarsToCredits(value) + if (credits <= 0) return undefined + return `${credits.toLocaleString()} credits` } function formatTtft(ms: number | undefined): string | undefined { @@ -208,6 +212,11 @@ function formatTps(outputTokens: number | undefined, durationMs: number): string return tps > 0 ? `${tps.toLocaleString('en-US')} tok/s` : undefined } +function getDisplayName(span: TraceSpan): string { + if (span.type?.toLowerCase() === 'tool') return normalizeToolId(span.name) + return span.name +} + /** * Flattens the visible (expanded) span tree into a linear list for keyboard * navigation, carrying depth, the chain of parent ids for indent drawing, and @@ -294,7 +303,7 @@ function findSpan(spans: TraceSpan[], id: string | null): TraceSpan | null { */ function spanMatchesQuery(span: TraceSpan, query: string): boolean { if (!query) return true - return (span.name ?? '').toLowerCase().includes(query.toLowerCase()) + return getDisplayName(span).toLowerCase().includes(query.toLowerCase()) } /** @@ -431,12 +440,12 @@ const TraceTreeRow = memo(function TraceTreeRow({ nameMatches && 'text-[var(--text-primary)]' )} > - {span.name} + {getDisplayName(span)}
- {span.name} + {getDisplayName(span)} {formatDuration(duration, { precision: 2 }) || '—'} {offsetMs > 0 && ` · +${formatDuration(offsetMs, { precision: 2 })}`} @@ -797,7 +806,7 @@ const TraceDetailPane = memo(function TraceDetailPane({ span }: { span: TraceSpa hasError ? 'text-[var(--text-error)]' : 'text-[var(--text-primary)]' )} > - {span.name} + {getDisplayName(span)}
diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index 3d173c6b4ca..c8a171f4d8a 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -556,7 +556,7 @@ async function applyHostedKeyCostToResult( * Knowledge tools: 'knowledge_search_' -> 'knowledge_search' * Table tools: 'table_query_rows_' -> 'table_query_rows' */ -function normalizeToolId(toolId: string): string { +export function normalizeToolId(toolId: string): string { if (toolId.startsWith('workflow_executor_') && toolId.length > 'workflow_executor_'.length) { return 'workflow_executor' } From ad5f50f3fd5a46d6b04f697ae367edc3ee396cf4 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 29 Apr 2026 13:34:13 -0700 Subject: [PATCH 2/9] fix(trace): fix sub-credit display, locale, and pluralization in formatCostAmount --- .../log-details/components/trace-view/trace-view.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx index 09317d49bd6..82a07ca20f9 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx @@ -195,8 +195,8 @@ function formatTokenCount(value: number | undefined): string | undefined { function formatCostAmount(value: number | undefined): string | undefined { if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined const credits = dollarsToCredits(value) - if (credits <= 0) return undefined - return `${credits.toLocaleString()} credits` + if (credits <= 0) return '<1 credit' + return `${credits.toLocaleString('en-US')} ${credits === 1 ? 'credit' : 'credits'}` } function formatTtft(ms: number | undefined): string | undefined { From 7ae61df9dba7332f120913c84ee11556d1c32e0b Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 29 Apr 2026 13:39:17 -0700 Subject: [PATCH 3/9] fix(trace): extract normalizeToolId to client-safe module, consolidate log-details formatting utils --- .../components/trace-spans/trace-spans.tsx | 77 ++----------------- .../components/trace-view/trace-view.tsx | 35 ++------- .../logs/components/log-details/utils.ts | 46 +++++++++++ apps/sim/tools/index.ts | 39 +--------- apps/sim/tools/normalize.ts | 40 ++++++++++ 5 files changed, 100 insertions(+), 137 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts create mode 100644 apps/sim/tools/normalize.ts diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx index 0a047c89c3f..c4cc7e74bbc 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx @@ -22,6 +22,12 @@ import { import { AgentSkillsIcon, WorkflowIcon } from '@/components/icons' import { cn } from '@/lib/core/utils/cn' import type { TraceSpan } from '@/lib/logs/types' +import { + formatTokensSummary, + formatTps, + formatTtft, + parseTime, +} from '@/app/workspace/[workspaceId]/logs/components/log-details/utils' import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config' import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config' import { getBlock, getBlockByToolName } from '@/blocks' @@ -59,53 +65,12 @@ function useSetToggle() { ) } -/** - * Formats a token count with locale-aware thousands separators. - * Returns `undefined` for missing or non-positive counts so callers can - * filter them out before rendering. - */ -function formatTokenCount(value: number | undefined): string | undefined { - if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined - return value.toLocaleString('en-US') -} - -/** - * Builds a compact, dot-separated token summary for a span: - * `"1,234 in · 567 out · 1,801 total"` with cache/reasoning appended when - * present. Returns `undefined` when the span has no meaningful token data. - */ -function formatTokensSummary(tokens: TraceSpan['tokens']): string | undefined { - if (!tokens) return undefined - const parts: string[] = [] - const input = formatTokenCount(tokens.input) - const output = formatTokenCount(tokens.output) - const total = formatTokenCount(tokens.total) - const cacheRead = formatTokenCount(tokens.cacheRead) - const cacheWrite = formatTokenCount(tokens.cacheWrite) - const reasoning = formatTokenCount(tokens.reasoning) - if (input) parts.push(`${input} in`) - if (cacheRead) parts.push(`${cacheRead} cached`) - if (cacheWrite) parts.push(`${cacheWrite} cache write`) - if (output) parts.push(`${output} out`) - if (reasoning) parts.push(`${reasoning} reasoning`) - if (total) parts.push(`${total} total`) - return parts.length > 0 ? parts.join(' · ') : undefined -} - -/** - * Formats a USD cost value for display. Shows `<$0.0001` for non-zero sub-cent - * amounts so the user sees it was counted. - */ function formatCostAmount(value: number | undefined): string | undefined { if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined if (value < 0.0001) return '<$0.0001' return `$${value.toFixed(4)}` } -/** - * Builds a compact cost summary: `"$0.0023 · $0.0001 in · $0.0022 out"`. - * Falls back to whichever parts are present. - */ function formatCostSummary(cost: TraceSpan['cost']): string | undefined { if (!cost) return undefined const parts: string[] = [] @@ -118,36 +83,6 @@ function formatCostSummary(cost: TraceSpan['cost']): string | undefined { return parts.length > 0 ? parts.join(' · ') : undefined } -/** - * Derives tokens-per-second from output tokens over segment duration. - * Returns `undefined` when inputs are missing or non-positive. - */ -function formatTps(outputTokens: number | undefined, durationMs: number): string | undefined { - if (typeof outputTokens !== 'number' || !(outputTokens > 0)) return undefined - if (!(durationMs > 0)) return undefined - const tps = Math.round(outputTokens / (durationMs / 1000)) - if (!(tps > 0)) return undefined - return `${tps.toLocaleString('en-US')} tok/s` -} - -/** - * Formats time-to-first-token. Uses `ms` below 1000, `s` above. - */ -function formatTtft(ms: number | undefined): string | undefined { - if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) return undefined - if (ms < 1000) return `${Math.round(ms)}ms` - return `${(ms / 1000).toFixed(2)}s` -} - -/** - * Parses a time value to milliseconds - */ -function parseTime(value?: string | number | null): number { - if (!value) return 0 - const ms = typeof value === 'number' ? value : new Date(value).getTime() - return Number.isFinite(ms) ? ms : 0 -} - /** * Checks if a span or any of its descendants has an error (any error). */ diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx index 82a07ca20f9..288f0a39d76 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx @@ -33,12 +33,18 @@ import { AgentSkillsIcon, WorkflowIcon } from '@/components/icons' import { dollarsToCredits } from '@/lib/billing/credits/conversion' import { cn } from '@/lib/core/utils/cn' import type { TraceSpan } from '@/lib/logs/types' +import { + formatTokenCount, + formatTps, + formatTtft, + parseTime, +} from '@/app/workspace/[workspaceId]/logs/components/log-details/utils' import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config' import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config' import { getBlock, getBlockByToolName } from '@/blocks' import { useCodeViewerFeatures } from '@/hooks/use-code-viewer' import { PROVIDER_DEFINITIONS } from '@/providers/models' -import { normalizeToolId } from '@/tools' +import { normalizeToolId } from '@/tools/normalize' const DEFAULT_BLOCK_COLOR = '#6b7280' const DEFAULT_TREE_PANE_WIDTH = 360 @@ -64,15 +70,6 @@ interface BlockAppearance { bgColor: string } -/** - * Parses a timestamp or numeric ms into milliseconds since epoch. - */ -function parseTime(value?: string | number | null): number { - if (!value) return 0 - const ms = typeof value === 'number' ? value : new Date(value).getTime() - return Number.isFinite(ms) ? ms : 0 -} - /** * Whether a span type represents a loop or parallel iteration container. */ @@ -187,11 +184,6 @@ function iconColorClass(bgColor: string): string { return r * 299 + g * 587 + b * 114 > 160_000 ? 'text-[#111111]' : 'text-white' } -function formatTokenCount(value: number | undefined): string | undefined { - if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined - return value.toLocaleString('en-US') -} - function formatCostAmount(value: number | undefined): string | undefined { if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined const credits = dollarsToCredits(value) @@ -199,19 +191,6 @@ function formatCostAmount(value: number | undefined): string | undefined { return `${credits.toLocaleString('en-US')} ${credits === 1 ? 'credit' : 'credits'}` } -function formatTtft(ms: number | undefined): string | undefined { - if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) return undefined - if (ms < 1000) return `${Math.round(ms)}ms` - return `${(ms / 1000).toFixed(2)}s` -} - -function formatTps(outputTokens: number | undefined, durationMs: number): string | undefined { - if (typeof outputTokens !== 'number' || !(outputTokens > 0)) return undefined - if (!(durationMs > 0)) return undefined - const tps = Math.round(outputTokens / (durationMs / 1000)) - return tps > 0 ? `${tps.toLocaleString('en-US')} tok/s` : undefined -} - function getDisplayName(span: TraceSpan): string { if (span.type?.toLowerCase() === 'tool') return normalizeToolId(span.name) return span.name diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts new file mode 100644 index 00000000000..0effa1e79ad --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts @@ -0,0 +1,46 @@ +import type { TraceSpan } from '@/lib/logs/types' + +export function parseTime(value?: string | number | null): number { + if (!value) return 0 + const ms = typeof value === 'number' ? value : new Date(value).getTime() + return Number.isFinite(ms) ? ms : 0 +} + +export function formatTokenCount(value: number | undefined): string | undefined { + if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined + return value.toLocaleString('en-US') +} + +export function formatTtft(ms: number | undefined): string | undefined { + if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) return undefined + if (ms < 1000) return `${Math.round(ms)}ms` + return `${(ms / 1000).toFixed(2)}s` +} + +export function formatTps( + outputTokens: number | undefined, + durationMs: number +): string | undefined { + if (typeof outputTokens !== 'number' || !(outputTokens > 0)) return undefined + if (!(durationMs > 0)) return undefined + const tps = Math.round(outputTokens / (durationMs / 1000)) + return tps > 0 ? `${tps.toLocaleString('en-US')} tok/s` : undefined +} + +export function formatTokensSummary(tokens: TraceSpan['tokens']): string | undefined { + if (!tokens) return undefined + const parts: string[] = [] + const input = formatTokenCount(tokens.input) + const output = formatTokenCount(tokens.output) + const total = formatTokenCount(tokens.total) + const cacheRead = formatTokenCount(tokens.cacheRead) + const cacheWrite = formatTokenCount(tokens.cacheWrite) + const reasoning = formatTokenCount(tokens.reasoning) + if (input) parts.push(`${input} in`) + if (cacheRead) parts.push(`${cacheRead} cached`) + if (cacheWrite) parts.push(`${cacheWrite} cache write`) + if (output) parts.push(`${output} out`) + if (reasoning) parts.push(`${reasoning} reasoning`) + if (total) parts.push(`${total} total`) + return parts.length > 0 ? parts.join(' · ') : undefined +} diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index c8a171f4d8a..f0d49796455 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -550,44 +550,7 @@ async function applyHostedKeyCostToResult( } } -/** - * Normalizes a tool ID by stripping resource ID suffix (UUID/tableId). - * Workflow tools: 'workflow_executor_' -> 'workflow_executor' - * Knowledge tools: 'knowledge_search_' -> 'knowledge_search' - * Table tools: 'table_query_rows_' -> 'table_query_rows' - */ -export function normalizeToolId(toolId: string): string { - if (toolId.startsWith('workflow_executor_') && toolId.length > 'workflow_executor_'.length) { - return 'workflow_executor' - } - - const knowledgeOps = ['knowledge_search', 'knowledge_upload_chunk', 'knowledge_create_document'] - for (const op of knowledgeOps) { - if (toolId.startsWith(`${op}_`) && toolId.length > op.length + 1) { - return op - } - } - - const tableOps = [ - 'table_query_rows', - 'table_insert_row', - 'table_batch_insert_rows', - 'table_update_row', - 'table_update_rows_by_filter', - 'table_delete_rows_by_filter', - 'table_upsert_row', - 'table_get_row', - 'table_delete_row', - 'table_get_schema', - ] - for (const op of tableOps) { - if (toolId.startsWith(`${op}_`) && toolId.length > op.length + 1) { - return op - } - } - - return toolId -} +export { normalizeToolId } from '@/tools/normalize' /** * Maximum request body size in bytes before we warn/error about size limits. diff --git a/apps/sim/tools/normalize.ts b/apps/sim/tools/normalize.ts new file mode 100644 index 00000000000..9082e79a5c0 --- /dev/null +++ b/apps/sim/tools/normalize.ts @@ -0,0 +1,40 @@ +/** + * Normalizes a tool ID by stripping resource ID suffix (UUID/tableId). + * Workflow tools: 'workflow_executor_' -> 'workflow_executor' + * Knowledge tools: 'knowledge_search_' -> 'knowledge_search' + * Table tools: 'table_query_rows_' -> 'table_query_rows' + * + * Pure string utility — no server dependencies, safe to import in client components. + */ +export function normalizeToolId(toolId: string): string { + if (toolId.startsWith('workflow_executor_') && toolId.length > 'workflow_executor_'.length) { + return 'workflow_executor' + } + + const knowledgeOps = ['knowledge_search', 'knowledge_upload_chunk', 'knowledge_create_document'] + for (const op of knowledgeOps) { + if (toolId.startsWith(`${op}_`) && toolId.length > op.length + 1) { + return op + } + } + + const tableOps = [ + 'table_query_rows', + 'table_insert_row', + 'table_batch_insert_rows', + 'table_update_row', + 'table_update_rows_by_filter', + 'table_delete_rows_by_filter', + 'table_upsert_row', + 'table_get_row', + 'table_delete_row', + 'table_get_schema', + ] + for (const op of tableOps) { + if (toolId.startsWith(`${op}_`) && toolId.length > op.length + 1) { + return op + } + } + + return toolId +} From 00f80940205e3f858160f798c4f23350c6f58edb Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 29 Apr 2026 13:44:43 -0700 Subject: [PATCH 4/9] refactor(trace): consolidate shared span utilities into log-details/utils --- .../components/trace-spans/trace-spans.tsx | 94 +------------------ .../components/trace-view/trace-view.tsx | 76 ++------------- .../logs/components/log-details/utils.ts | 61 ++++++++++++ 3 files changed, 71 insertions(+), 160 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx index c4cc7e74bbc..d2d4e1ba977 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx @@ -19,32 +19,24 @@ import { Search as SearchIcon, Tooltip, } from '@/components/emcn' -import { AgentSkillsIcon, WorkflowIcon } from '@/components/icons' import { cn } from '@/lib/core/utils/cn' import type { TraceSpan } from '@/lib/logs/types' import { formatTokensSummary, formatTps, formatTtft, + getBlockIconAndColor, + hasErrorInTree, + hasUnhandledErrorInTree, + isIterationType, parseTime, } from '@/app/workspace/[workspaceId]/logs/components/log-details/utils' -import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config' -import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config' -import { getBlock, getBlockByToolName } from '@/blocks' import { useCodeViewerFeatures } from '@/hooks/use-code-viewer' interface TraceSpansProps { traceSpans?: TraceSpan[] } -/** - * Checks if a span type is a loop or parallel iteration - */ -function isIterationType(type: string): boolean { - const lower = type?.toLowerCase() || '' - return lower === 'loop-iteration' || lower === 'parallel-iteration' -} - /** * Creates a toggle handler for Set-based state */ @@ -83,37 +75,6 @@ function formatCostSummary(cost: TraceSpan['cost']): string | undefined { return parts.length > 0 ? parts.join(' · ') : undefined } -/** - * Checks if a span or any of its descendants has an error (any error). - */ -function hasErrorInTree(span: TraceSpan): boolean { - if (span.status === 'error') return true - if (span.children && span.children.length > 0) { - return span.children.some((child) => hasErrorInTree(child)) - } - if (span.toolCalls && span.toolCalls.length > 0) { - return span.toolCalls.some((tc) => tc.error) - } - return false -} - -/** - * Checks if a span or any of its descendants has an unhandled error. - * Spans with errorHandled: true (including containers that propagate it) - * are skipped. Used only for the root workflow span to match the actual - * workflow status. - */ -function hasUnhandledErrorInTree(span: TraceSpan): boolean { - if (span.status === 'error' && !span.errorHandled) return true - if (span.children && span.children.length > 0) { - return span.children.some((child) => hasUnhandledErrorInTree(child)) - } - if (span.toolCalls && span.toolCalls.length > 0 && !span.errorHandled) { - return span.toolCalls.some((tc) => tc.error) - } - return false -} - /** * Normalizes and sorts trace spans recursively. * Deduplicates children and sorts by start time. @@ -136,53 +97,6 @@ function normalizeAndSortSpans(spans: TraceSpan[]): TraceSpan[] { }) } -const DEFAULT_BLOCK_COLOR = '#6b7280' - -/** - * Gets icon and color for a span type using block config - */ -function getBlockIconAndColor( - type: string, - toolName?: string -): { - icon: React.ComponentType<{ className?: string }> | null - bgColor: string -} { - const lowerType = type.toLowerCase() - - // Check for tool by name first (most specific) - if (lowerType === 'tool' && toolName) { - // Handle load_skill tool with the AgentSkillsIcon - if (toolName === 'load_skill') { - return { icon: AgentSkillsIcon, bgColor: '#8B5CF6' } - } - const toolBlock = getBlockByToolName(toolName) - if (toolBlock) { - return { icon: toolBlock.icon, bgColor: toolBlock.bgColor } - } - } - - // Special types not in block registry - if (lowerType === 'loop' || lowerType === 'loop-iteration') { - return { icon: LoopTool.icon, bgColor: LoopTool.bgColor } - } - if (lowerType === 'parallel' || lowerType === 'parallel-iteration') { - return { icon: ParallelTool.icon, bgColor: ParallelTool.bgColor } - } - if (lowerType === 'workflow') { - return { icon: WorkflowIcon, bgColor: '#6366F1' } - } - - // Look up from block registry (model maps to agent) - const blockType = lowerType === 'model' ? 'agent' : lowerType - const blockConfig = getBlock(blockType) - if (blockConfig) { - return { icon: blockConfig.icon, bgColor: blockConfig.bgColor } - } - - return { icon: null, bgColor: DEFAULT_BLOCK_COLOR } -} - /** * Renders the progress bar showing execution timeline */ diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx index 288f0a39d76..6a47a3be28d 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx @@ -29,7 +29,6 @@ import { Search as SearchIcon, Tooltip, } from '@/components/emcn' -import { AgentSkillsIcon, WorkflowIcon } from '@/components/icons' import { dollarsToCredits } from '@/lib/billing/credits/conversion' import { cn } from '@/lib/core/utils/cn' import type { TraceSpan } from '@/lib/logs/types' @@ -37,16 +36,15 @@ import { formatTokenCount, formatTps, formatTtft, + getBlockIconAndColor, + hasErrorInTree, + hasUnhandledErrorInTree, + isIterationType, parseTime, } from '@/app/workspace/[workspaceId]/logs/components/log-details/utils' -import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config' -import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config' -import { getBlock, getBlockByToolName } from '@/blocks' import { useCodeViewerFeatures } from '@/hooks/use-code-viewer' -import { PROVIDER_DEFINITIONS } from '@/providers/models' import { normalizeToolId } from '@/tools/normalize' -const DEFAULT_BLOCK_COLOR = '#6b7280' const DEFAULT_TREE_PANE_WIDTH = 360 const MIN_TREE_PANE_WIDTH = 200 const MAX_TREE_PANE_WIDTH = 600 @@ -65,19 +63,6 @@ interface FlatSpanEntry { parentDuration?: number } -interface BlockAppearance { - icon: React.ComponentType<{ className?: string }> | null - bgColor: string -} - -/** - * Whether a span type represents a loop or parallel iteration container. - */ -function isIterationType(type: string): boolean { - const lower = type?.toLowerCase() || '' - return lower === 'loop-iteration' || lower === 'parallel-iteration' -} - /** * Returns the stable id for a span, synthesized when absent. */ @@ -85,27 +70,6 @@ function getSpanId(span: TraceSpan): string { return span.id || `span-${span.name}-${span.startTime}` } -/** - * Walks a span's descendants to determine if any error exists in the subtree. - */ -function hasErrorInTree(span: TraceSpan): boolean { - if (span.status === 'error') return true - if (span.children?.length) return span.children.some(hasErrorInTree) - if (span.toolCalls?.length) return span.toolCalls.some((tc) => tc.error) - return false -} - -/** - * Like `hasErrorInTree` but only counts errors that were not handled by an - * error-handler path. Used for the root workflow status color. - */ -function hasUnhandledErrorInTree(span: TraceSpan): boolean { - if (span.status === 'error' && !span.errorHandled) return true - if (span.children?.length) return span.children.some(hasUnhandledErrorInTree) - if (span.toolCalls?.length && !span.errorHandled) return span.toolCalls.some((tc) => tc.error) - return false -} - /** * Normalizes and sorts a tree of spans by start time. */ @@ -146,34 +110,6 @@ function getDisplayChildren(span: TraceSpan): TraceSpan[] { return kids } -/** - * Resolves the block icon and accent color for a trace span type. - */ -function getBlockAppearance(type: string, toolName?: string, provider?: string): BlockAppearance { - const lowerType = type.toLowerCase() - if (lowerType === 'tool' && toolName) { - const normalized = normalizeToolId(toolName) - if (normalized === 'load_skill') return { icon: AgentSkillsIcon, bgColor: '#8B5CF6' } - const toolBlock = getBlockByToolName(normalized) - if (toolBlock) return { icon: toolBlock.icon, bgColor: toolBlock.bgColor } - } - if (lowerType === 'loop' || lowerType === 'loop-iteration') - return { icon: LoopTool.icon, bgColor: LoopTool.bgColor } - if (lowerType === 'parallel' || lowerType === 'parallel-iteration') - return { icon: ParallelTool.icon, bgColor: ParallelTool.bgColor } - if (lowerType === 'workflow') return { icon: WorkflowIcon, bgColor: '#6366F1' } - if (lowerType === 'model' && provider) { - const providerDef = PROVIDER_DEFINITIONS[provider] - if (providerDef?.icon) { - return { icon: providerDef.icon, bgColor: providerDef.color ?? DEFAULT_BLOCK_COLOR } - } - } - const blockType = lowerType === 'model' ? 'agent' : lowerType - const blockConfig = getBlock(blockType) - if (blockConfig) return { icon: blockConfig.icon, bgColor: blockConfig.bgColor } - return { icon: null, bgColor: DEFAULT_BLOCK_COLOR } -} - /** Returns 'text-white' for dark backgrounds, dark text for light ones. */ function iconColorClass(bgColor: string): string { const hex = bgColor.replace('#', '') @@ -344,7 +280,7 @@ const TraceTreeRow = memo(function TraceTreeRow({ const duration = span.duration || endMs - startMs const isRootWorkflow = depth === 0 && span.type?.toLowerCase() === 'workflow' const hasError = isRootWorkflow ? hasUnhandledErrorInTree(span) : hasErrorInTree(span) - const { icon: BlockIcon, bgColor } = getBlockAppearance(span.type, span.name, span.provider) + const { icon: BlockIcon, bgColor } = getBlockIconAndColor(span.type, span.name, span.provider) const nameMatches = !!matchQuery && spanMatchesQuery(span, matchQuery) const offsetMs = runStartMs > 0 ? Math.max(0, startMs - runStartMs) : 0 @@ -726,7 +662,7 @@ const TraceDetailPane = memo(function TraceDetailPane({ span }: { span: TraceSpa } const duration = span.duration || parseTime(span.endTime) - parseTime(span.startTime) - const { icon: BlockIcon, bgColor } = getBlockAppearance(span.type, span.name, span.provider) + const { icon: BlockIcon, bgColor } = getBlockIconAndColor(span.type, span.name, span.provider) const isRootWorkflow = span.type?.toLowerCase() === 'workflow' const hasError = isRootWorkflow ? hasUnhandledErrorInTree(span) : hasErrorInTree(span) const isDirectError = span.status === 'error' diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts index 0effa1e79ad..76ad708391f 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts @@ -1,4 +1,65 @@ +import type React from 'react' +import { AgentSkillsIcon, WorkflowIcon } from '@/components/icons' import type { TraceSpan } from '@/lib/logs/types' +import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config' +import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config' +import { getBlock, getBlockByToolName } from '@/blocks' +import { PROVIDER_DEFINITIONS } from '@/providers/models' +import { normalizeToolId } from '@/tools/normalize' + +export const DEFAULT_BLOCK_COLOR = '#6b7280' + +export interface BlockIconAndColor { + icon: React.ComponentType<{ className?: string }> | null + bgColor: string +} + +export function isIterationType(type: string): boolean { + const lower = type?.toLowerCase() || '' + return lower === 'loop-iteration' || lower === 'parallel-iteration' +} + +export function hasErrorInTree(span: TraceSpan): boolean { + if (span.status === 'error') return true + if (span.children?.length) return span.children.some(hasErrorInTree) + if (span.toolCalls?.length) return span.toolCalls.some((tc) => tc.error) + return false +} + +export function hasUnhandledErrorInTree(span: TraceSpan): boolean { + if (span.status === 'error' && !span.errorHandled) return true + if (span.children?.length) return span.children.some(hasUnhandledErrorInTree) + if (span.toolCalls?.length && !span.errorHandled) return span.toolCalls.some((tc) => tc.error) + return false +} + +export function getBlockIconAndColor( + type: string, + toolName?: string, + provider?: string +): BlockIconAndColor { + const lowerType = type.toLowerCase() + if (lowerType === 'tool' && toolName) { + const normalized = normalizeToolId(toolName) + if (normalized === 'load_skill') return { icon: AgentSkillsIcon, bgColor: '#8B5CF6' } + const toolBlock = getBlockByToolName(normalized) + if (toolBlock) return { icon: toolBlock.icon, bgColor: toolBlock.bgColor } + } + if (lowerType === 'loop' || lowerType === 'loop-iteration') + return { icon: LoopTool.icon, bgColor: LoopTool.bgColor } + if (lowerType === 'parallel' || lowerType === 'parallel-iteration') + return { icon: ParallelTool.icon, bgColor: ParallelTool.bgColor } + if (lowerType === 'workflow') return { icon: WorkflowIcon, bgColor: '#6366F1' } + if (lowerType === 'model' && provider) { + const providerDef = PROVIDER_DEFINITIONS[provider] + if (providerDef?.icon) + return { icon: providerDef.icon, bgColor: providerDef.color ?? DEFAULT_BLOCK_COLOR } + } + const blockType = lowerType === 'model' ? 'agent' : lowerType + const blockConfig = getBlock(blockType) + if (blockConfig) return { icon: blockConfig.icon, bgColor: blockConfig.bgColor } + return { icon: null, bgColor: DEFAULT_BLOCK_COLOR } +} export function parseTime(value?: string | number | null): number { if (!value) return 0 From 6a1a28c49f59d7b2b54dcfcecd18236702dc92e9 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 29 Apr 2026 13:51:01 -0700 Subject: [PATCH 5/9] fix(tools): import normalizeToolId for internal use after extracting to normalize.ts --- apps/sim/tools/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index f0d49796455..0a665bc3e4e 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -550,6 +550,8 @@ async function applyHostedKeyCostToResult( } } +import { normalizeToolId } from '@/tools/normalize' + export { normalizeToolId } from '@/tools/normalize' /** From 005464e210ca1f1a34ebd216195fb60f124291c2 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 29 Apr 2026 13:54:39 -0700 Subject: [PATCH 6/9] style(trace-spans): replace inline transform style with Tailwind rotate-180 on disclosure chevron --- .../log-details/components/trace-spans/trace-spans.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx index d2d4e1ba977..288ac064526 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx @@ -252,10 +252,10 @@ function InputOutputSection({ {label}
{isExpanded && ( From 669c35effaa55ed17d427d287c99a803b38a3fbb Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 29 Apr 2026 14:01:04 -0700 Subject: [PATCH 7/9] fix(trace-spans): show credits instead of dollars for cost display --- .../log-details/components/trace-spans/trace-spans.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx index 288ac064526..2101da36372 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx @@ -19,6 +19,7 @@ import { Search as SearchIcon, Tooltip, } from '@/components/emcn' +import { dollarsToCredits } from '@/lib/billing/credits/conversion' import { cn } from '@/lib/core/utils/cn' import type { TraceSpan } from '@/lib/logs/types' import { @@ -59,8 +60,9 @@ function useSetToggle() { function formatCostAmount(value: number | undefined): string | undefined { if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined - if (value < 0.0001) return '<$0.0001' - return `$${value.toFixed(4)}` + const credits = dollarsToCredits(value) + if (credits <= 0) return '<1 credit' + return `${credits.toLocaleString('en-US')} ${credits === 1 ? 'credit' : 'credits'}` } function formatCostSummary(cost: TraceSpan['cost']): string | undefined { From e8f7139068bc75ab10a6db654ecce9e46d832790 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 29 Apr 2026 14:49:59 -0700 Subject: [PATCH 8/9] refactor(trace): move formatCostAmount and getDisplayName to shared utils, normalize tool names in trace-spans --- .../components/trace-spans/trace-spans.tsx | 12 +++--------- .../components/trace-view/trace-view.tsx | 16 ++-------------- .../logs/components/log-details/utils.ts | 13 +++++++++++++ 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx index 2101da36372..bfb14ed08ba 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx @@ -19,14 +19,15 @@ import { Search as SearchIcon, Tooltip, } from '@/components/emcn' -import { dollarsToCredits } from '@/lib/billing/credits/conversion' import { cn } from '@/lib/core/utils/cn' import type { TraceSpan } from '@/lib/logs/types' import { + formatCostAmount, formatTokensSummary, formatTps, formatTtft, getBlockIconAndColor, + getDisplayName, hasErrorInTree, hasUnhandledErrorInTree, isIterationType, @@ -58,13 +59,6 @@ function useSetToggle() { ) } -function formatCostAmount(value: number | undefined): string | undefined { - if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined - const credits = dollarsToCredits(value) - if (credits <= 0) return '<1 credit' - return `${credits.toLocaleString('en-US')} ${credits === 1 ? 'credit' : 'credits'}` -} - function formatCostSummary(cost: TraceSpan['cost']): string | undefined { if (!cost) return undefined const parts: string[] = [] @@ -535,7 +529,7 @@ const TraceSpanNode = memo(function TraceSpanNode({ className='min-w-0 max-w-[180px] truncate font-medium text-caption' style={{ color: showErrorStyle ? 'var(--text-error)' : 'var(--text-secondary)' }} > - {span.name} + {getDisplayName(span)}
{isToggleable && ( 160_000 ? 'text-[#111111]' : 'text-white' } -function formatCostAmount(value: number | undefined): string | undefined { - if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined - const credits = dollarsToCredits(value) - if (credits <= 0) return '<1 credit' - return `${credits.toLocaleString('en-US')} ${credits === 1 ? 'credit' : 'credits'}` -} - -function getDisplayName(span: TraceSpan): string { - if (span.type?.toLowerCase() === 'tool') return normalizeToolId(span.name) - return span.name -} - /** * Flattens the visible (expanded) span tree into a linear list for keyboard * navigation, carrying depth, the chain of parent ids for indent drawing, and diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts index 76ad708391f..afdf52bdaab 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts @@ -1,5 +1,6 @@ import type React from 'react' import { AgentSkillsIcon, WorkflowIcon } from '@/components/icons' +import { dollarsToCredits } from '@/lib/billing/credits/conversion' import type { TraceSpan } from '@/lib/logs/types' import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config' import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config' @@ -88,6 +89,18 @@ export function formatTps( return tps > 0 ? `${tps.toLocaleString('en-US')} tok/s` : undefined } +export function getDisplayName(span: TraceSpan): string { + if (span.type?.toLowerCase() === 'tool') return normalizeToolId(span.name) + return span.name +} + +export function formatCostAmount(value: number | undefined): string | undefined { + if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined + const credits = dollarsToCredits(value) + if (credits <= 0) return '<1 credit' + return `${credits.toLocaleString('en-US')} ${credits === 1 ? 'credit' : 'credits'}` +} + export function formatTokensSummary(tokens: TraceSpan['tokens']): string | undefined { if (!tokens) return undefined const parts: string[] = [] From 4c07eb9783c29ba738008751110122bf2edce38c Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 29 Apr 2026 15:14:01 -0700 Subject: [PATCH 9/9] fix(trace-spans): simplify formatCostSummary to show total credits only --- .../log-details/components/trace-spans/trace-spans.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx index bfb14ed08ba..59e1e3bc008 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-spans/trace-spans.tsx @@ -61,14 +61,7 @@ function useSetToggle() { function formatCostSummary(cost: TraceSpan['cost']): string | undefined { if (!cost) return undefined - const parts: string[] = [] - const total = formatCostAmount(cost.total) - const input = formatCostAmount(cost.input) - const output = formatCostAmount(cost.output) - if (total) parts.push(total) - if (input) parts.push(`${input} in`) - if (output) parts.push(`${output} out`) - return parts.length > 0 ? parts.join(' · ') : undefined + return formatCostAmount(cost.total) } /**