-
Trace Span
-
- {normalizedSpans.map((span, index) => (
-
- ))}
-
+
+ {normalizedSpans.map((span, index) => (
+
+ ))}
)
})
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx
index 08643c051d..38f9317a1f 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx
@@ -1,10 +1,23 @@
'use client'
-import { memo, useEffect, useMemo, useRef, useState } from 'react'
-import { ChevronUp, X } from 'lucide-react'
-import { Button, Eye } from '@/components/emcn'
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { ArrowDown, ArrowUp, Check, ChevronUp, Clipboard, Search, X } from 'lucide-react'
+import { createPortal } from 'react-dom'
+import {
+ Button,
+ Code,
+ Eye,
+ Input,
+ Popover,
+ PopoverAnchor,
+ PopoverContent,
+ PopoverDivider,
+ PopoverItem,
+ Tooltip,
+} from '@/components/emcn'
import { ScrollArea } from '@/components/ui/scroll-area'
import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants'
+import { cn } from '@/lib/core/utils/cn'
import {
ExecutionSnapshot,
FileCards,
@@ -17,11 +30,194 @@ import {
StatusBadge,
TriggerBadge,
} from '@/app/workspace/[workspaceId]/logs/utils'
+import { useCodeViewerFeatures } from '@/hooks/use-code-viewer'
import { usePermissionConfig } from '@/hooks/use-permission-config'
import { formatCost } from '@/providers/utils'
import type { WorkflowLog } from '@/stores/logs/filters/types'
import { useLogDetailsUIStore } from '@/stores/logs/store'
+/**
+ * Workflow Output section with code viewer, copy, search, and context menu functionality
+ */
+function WorkflowOutputSection({ output }: { output: Record
}) {
+ const contentRef = useRef(null)
+ const [copied, setCopied] = useState(false)
+
+ // Context menu state
+ const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)
+ const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 })
+
+ const {
+ isSearchActive,
+ searchQuery,
+ setSearchQuery,
+ matchCount,
+ currentMatchIndex,
+ activateSearch,
+ closeSearch,
+ goToNextMatch,
+ goToPreviousMatch,
+ handleMatchCountChange,
+ searchInputRef,
+ } = useCodeViewerFeatures({ contentRef })
+
+ const jsonString = useMemo(() => JSON.stringify(output, null, 2), [output])
+
+ const handleContextMenu = useCallback((e: React.MouseEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+ setContextMenuPosition({ x: e.clientX, y: e.clientY })
+ setIsContextMenuOpen(true)
+ }, [])
+
+ const closeContextMenu = useCallback(() => {
+ setIsContextMenuOpen(false)
+ }, [])
+
+ const handleCopy = useCallback(() => {
+ navigator.clipboard.writeText(jsonString)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 1500)
+ closeContextMenu()
+ }, [jsonString, closeContextMenu])
+
+ const handleSearch = useCallback(() => {
+ activateSearch()
+ closeContextMenu()
+ }, [activateSearch, closeContextMenu])
+
+ return (
+
+
+
+ {/* Glass action buttons overlay */}
+ {!isSearchActive && (
+
+
+
+
+
+ {copied ? 'Copied' : 'Copy'}
+
+
+
+
+
+ Search
+
+
+ )}
+
+
+ {/* Search Overlay */}
+ {isSearchActive && (
+
e.stopPropagation()}
+ >
+
setSearchQuery(e.target.value)}
+ placeholder='Search...'
+ className='mr-[2px] h-[23px] w-[94px] text-[12px]'
+ />
+
0 ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)]'
+ )}
+ >
+ {matchCount > 0 ? `${currentMatchIndex + 1}/${matchCount}` : '0/0'}
+
+
+
+
+
+ )}
+
+ {/* Context Menu - rendered in portal to avoid transform/overflow clipping */}
+ {typeof document !== 'undefined' &&
+ createPortal(
+
+
+
+ Copy
+
+ Search
+
+ ,
+ document.body
+ )}
+
+ )
+}
+
interface LogDetailsProps {
/** The log to display details for */
log: WorkflowLog | null
@@ -78,6 +274,18 @@ export const LogDetails = memo(function LogDetails({
return isWorkflowExecutionLog && log?.cost
}, [log, isWorkflowExecutionLog])
+ // Extract and clean the workflow final output (remove childTraceSpans for cleaner display)
+ const workflowOutput = useMemo(() => {
+ const executionData = log?.executionData as
+ | { finalOutput?: Record }
+ | undefined
+ if (!executionData?.finalOutput) return null
+ const { childTraceSpans, ...cleanOutput } = executionData.finalOutput as {
+ childTraceSpans?: unknown
+ } & Record
+ return cleanOutput
+ }, [log?.executionData])
+
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
@@ -87,12 +295,12 @@ export const LogDetails = memo(function LogDetails({
if (isOpen) {
if (e.key === 'ArrowUp' && hasPrev && onNavigatePrev) {
e.preventDefault()
- handleNavigate(onNavigatePrev)
+ onNavigatePrev()
}
if (e.key === 'ArrowDown' && hasNext && onNavigateNext) {
e.preventDefault()
- handleNavigate(onNavigateNext)
+ onNavigateNext()
}
}
}
@@ -101,10 +309,6 @@ export const LogDetails = memo(function LogDetails({
return () => window.removeEventListener('keydown', handleKeyDown)
}, [isOpen, onClose, hasPrev, hasNext, onNavigatePrev, onNavigateNext])
- const handleNavigate = (navigateFunction: () => void) => {
- navigateFunction()
- }
-
const formattedTimestamp = useMemo(
() => (log ? formatDate(log.createdAt) : null),
[log?.createdAt]
@@ -142,7 +346,7 @@ export const LogDetails = memo(function LogDetails({