From 7386d227eb45b23f594ecb5e1decdc103103b0d6 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 2 Feb 2026 14:53:14 -0800 Subject: [PATCH] fix(formatting): consolidate duration formatting into shared utility --- .../logs/components/logs-list/logs-list.tsx | 2 +- .../app/workspace/[workspaceId]/logs/utils.ts | 36 +---------------- .../thinking-block/thinking-block.tsx | 9 +---- .../components/tool-call/tool-call.tsx | 8 +--- .../components/terminal/terminal.tsx | 2 +- .../[workflowId]/components/terminal/utils.ts | 11 ----- .../training-modal/training-modal.tsx | 5 ++- .../workspace-notification-delivery.ts | 7 +--- apps/sim/components/ui/tool-call.tsx | 6 +-- apps/sim/lib/core/utils/formatting.ts | 40 +++++++++++++++---- 10 files changed, 46 insertions(+), 80 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx index 5a073a04b..8f02ebf14 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx @@ -6,11 +6,11 @@ import Link from 'next/link' import { List, type RowComponentProps, useListRef } from 'react-window' import { Badge, buttonVariants } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' +import { formatDuration } from '@/lib/core/utils/formatting' import { DELETED_WORKFLOW_COLOR, DELETED_WORKFLOW_LABEL, formatDate, - formatDuration, getDisplayStatus, LOG_COLUMNS, StatusBadge, diff --git a/apps/sim/app/workspace/[workspaceId]/logs/utils.ts b/apps/sim/app/workspace/[workspaceId]/logs/utils.ts index d21561b97..1a35685c8 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/utils.ts @@ -1,6 +1,7 @@ import React from 'react' import { format } from 'date-fns' import { Badge } from '@/components/emcn' +import { formatDuration } from '@/lib/core/utils/formatting' import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options' import { getBlock } from '@/blocks/registry' import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types' @@ -362,47 +363,14 @@ export function mapToExecutionLogAlt(log: RawLogResponse): ExecutionLog { } } -/** - * Format duration for display in logs UI - * If duration is under 1 second, displays as milliseconds (e.g., "500ms") - * If duration is 1 second or more, displays as seconds (e.g., "1.23s") - * @param duration - Duration string (e.g., "500ms") or null - * @returns Formatted duration string or null - */ -export function formatDuration(duration: string | null): string | null { - if (!duration) return null - - // Extract numeric value from duration string (e.g., "500ms" -> 500) - const ms = Number.parseInt(duration.replace(/[^0-9]/g, ''), 10) - - if (!Number.isFinite(ms)) return duration - - if (ms < 1000) { - return `${ms}ms` - } - - // Convert to seconds with up to 2 decimal places - const seconds = ms / 1000 - return `${seconds.toFixed(2).replace(/\.?0+$/, '')}s` -} - /** * Format latency value for display in dashboard UI - * If latency is under 1 second, displays as milliseconds (e.g., "500ms") - * If latency is 1 second or more, displays as seconds (e.g., "1.23s") * @param ms - Latency in milliseconds (number) * @returns Formatted latency string */ export function formatLatency(ms: number): string { if (!Number.isFinite(ms) || ms <= 0) return '—' - - if (ms < 1000) { - return `${Math.round(ms)}ms` - } - - // Convert to seconds with up to 2 decimal places - const seconds = ms / 1000 - return `${seconds.toFixed(2).replace(/\.?0+$/, '')}s` + return formatDuration(ms, { precision: 2 }) } export const formatDate = (dateString: string) => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx index de632ca5f..ef92e1b43 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx @@ -3,6 +3,7 @@ import { memo, useEffect, useMemo, useRef, useState } from 'react' import clsx from 'clsx' import { ChevronUp } from 'lucide-react' +import { formatDuration } from '@/lib/core/utils/formatting' import { CopilotMarkdownRenderer } from '../markdown-renderer' /** Removes thinking tags (raw or escaped) and special tags from streamed content */ @@ -241,15 +242,9 @@ export function ThinkingBlock({ return () => window.clearInterval(intervalId) }, [isStreaming, isExpanded, userHasScrolledAway]) - /** Formats duration in milliseconds to seconds (minimum 1s) */ - const formatDuration = (ms: number) => { - const seconds = Math.max(1, Math.round(ms / 1000)) - return `${seconds}s` - } - const hasContent = cleanContent.length > 0 const isThinkingDone = !isStreaming || hasFollowingContent || hasSpecialTags - const durationText = `${label} for ${formatDuration(duration)}` + const durationText = `${label} for ${formatDuration(Math.max(1000, duration))}` const getStreamingLabel = (lbl: string) => { if (lbl === 'Thought') return 'Thinking' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx index d22542375..997d9e60e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx @@ -15,6 +15,7 @@ import { hasInterrupt as hasInterruptFromConfig, isSpecialTool as isSpecialToolFromConfig, } from '@/lib/copilot/tools/client/ui-config' +import { formatDuration } from '@/lib/core/utils/formatting' import { CopilotMarkdownRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer' import { SmoothStreamingText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming' import { ThinkingBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block' @@ -848,13 +849,8 @@ const SubagentContentRenderer = memo(function SubagentContentRenderer({ (allParsed.options && Object.keys(allParsed.options).length > 0) ) - const formatDuration = (ms: number) => { - const seconds = Math.max(1, Math.round(ms / 1000)) - return `${seconds}s` - } - const outerLabel = getSubagentCompletionLabel(toolCall.name) - const durationText = `${outerLabel} for ${formatDuration(duration)}` + const durationText = `${outerLabel} for ${formatDuration(Math.max(1000, duration))}` const renderCollapsibleContent = () => ( <> diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx index abe60c6a4..21b1c2834 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx @@ -24,6 +24,7 @@ import { Tooltip, } from '@/components/emcn' import { getEnv, isTruthy } from '@/lib/core/config/env' +import { formatDuration } from '@/lib/core/utils/formatting' import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils' import { @@ -43,7 +44,6 @@ import { type EntryNode, type ExecutionGroup, flattenBlockEntriesOnly, - formatDuration, getBlockColor, getBlockIcon, groupEntriesByExecution, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts index 6dbc15770..18b8cfef6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts @@ -53,17 +53,6 @@ export function getBlockColor(blockType: string): string { return '#6b7280' } -/** - * Formats duration from milliseconds to readable format - */ -export function formatDuration(ms?: number): string { - if (ms === undefined || ms === null) return '-' - if (ms < 1000) { - return `${Math.round(ms)}ms` - } - return `${(ms / 1000).toFixed(2)}s` -} - /** * Determines if a keyboard event originated from a text-editable element */ diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/training-modal/training-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/training-modal/training-modal.tsx index cbdac251f..3003d4acd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/training-modal/training-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/training-modal/training-modal.tsx @@ -30,6 +30,7 @@ import { Textarea, } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' +import { formatDuration } from '@/lib/core/utils/formatting' import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer' import { formatEditSequence } from '@/lib/workflows/training/compute-edit-sequence' import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow' @@ -575,7 +576,9 @@ export function TrainingModal() { Duration:{' '} {dataset.metadata?.duration - ? `${(dataset.metadata.duration / 1000).toFixed(1)}s` + ? formatDuration(dataset.metadata.duration, { + precision: 1, + }) : 'N/A'} diff --git a/apps/sim/background/workspace-notification-delivery.ts b/apps/sim/background/workspace-notification-delivery.ts index d5dbf3a92..c6a72f011 100644 --- a/apps/sim/background/workspace-notification-delivery.ts +++ b/apps/sim/background/workspace-notification-delivery.ts @@ -19,6 +19,7 @@ import { checkUsageStatus } from '@/lib/billing/calculations/usage-monitor' import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' import { RateLimiter } from '@/lib/core/rate-limiter' import { decryptSecret } from '@/lib/core/security/encryption' +import { formatDuration } from '@/lib/core/utils/formatting' import { getBaseUrl } from '@/lib/core/utils/urls' import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types' import { sendEmail } from '@/lib/messaging/email/mailer' @@ -227,12 +228,6 @@ async function deliverWebhook( } } -function formatDuration(ms: number): string { - if (ms < 1000) return `${ms}ms` - if (ms < 60000) return `${(ms / 1000).toFixed(1)}s` - return `${(ms / 60000).toFixed(1)}m` -} - function formatCost(cost?: Record): string { if (!cost?.total) return 'N/A' const total = cost.total as number diff --git a/apps/sim/components/ui/tool-call.tsx b/apps/sim/components/ui/tool-call.tsx index b6d76ca7e..ade15ebe9 100644 --- a/apps/sim/components/ui/tool-call.tsx +++ b/apps/sim/components/ui/tool-call.tsx @@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' import type { ToolCallGroup, ToolCallState } from '@/lib/copilot/types' import { cn } from '@/lib/core/utils/cn' +import { formatDuration } from '@/lib/core/utils/formatting' interface ToolCallProps { toolCall: ToolCallState @@ -225,11 +226,6 @@ export function ToolCallCompletion({ toolCall, isCompact = false }: ToolCallProp const isError = toolCall.state === 'error' const isAborted = toolCall.state === 'aborted' - const formatDuration = (duration?: number) => { - if (!duration) return '' - return duration < 1000 ? `${duration}ms` : `${(duration / 1000).toFixed(1)}s` - } - return (
0 ? `${seconds.toFixed(precision)}s` : `${Math.floor(seconds)}s` }