Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/sim/app/(landing)/partners/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { Metadata } from 'next'
import Link from 'next/link'
import { getNavBlogPosts } from '@/lib/blog/registry'
import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
import { season } from '@/app/_styles/fonts/season/season'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -711,11 +711,24 @@ export const StructuredOutput = memo(function StructuredOutput({
}, [])

// Reset expanded paths when data changes
const prevDataJsonRef = useRef<string>('')

useEffect(() => {
if (prevDataRef.current !== data || prevIsErrorRef.current !== isError) {
if (prevIsErrorRef.current !== isError) {
prevDataRef.current = data
prevIsErrorRef.current = isError
prevDataJsonRef.current = JSON.stringify(data)
setExpandedPaths(computeInitialPaths(data, isError))
return
}

if (prevDataRef.current !== data) {
const newJson = JSON.stringify(data)
if (prevDataJsonRef.current !== newJson) {
prevDataJsonRef.current = newJson
setExpandedPaths(computeInitialPaths(data, isError))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unguarded JSON.stringify can throw on non-serializable data

Medium Severity

The new deep-equality check calls JSON.stringify(data) without a try-catch. If data contains circular references, BigInt values, or other non-JSON-serializable structures, this will throw an unhandled error and crash the structured output component. The previous code only compared by reference (prevDataRef.current !== data), which can never throw.

Additional Locations (1)
Fix in Cursor Fix in Web

prevDataRef.current = data
}
}, [data, isError])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,18 @@ export function useTerminalFilters() {
*/
const filterEntries = useCallback(
(entries: ConsoleEntry[]): ConsoleEntry[] => {
// Apply filters first
if (!hasActiveFilters && sortConfig.direction === 'desc') {
return entries
}

let result = entries

if (hasActiveFilters) {
result = entries.filter((entry) => {
// Block ID filter
if (filters.blockIds.size > 0 && !filters.blockIds.has(entry.blockId)) {
return false
}

// Status filter
if (filters.statuses.size > 0) {
const isError = !!entry.error
const hasStatus = isError ? filters.statuses.has('error') : filters.statuses.has('info')
Expand All @@ -105,7 +106,6 @@ export function useTerminalFilters() {
})
}

// Sort by executionOrder (monotonically increasing integer from server)
result = [...result].sort((a, b) => {
const comparison = a.executionOrder - b.executionOrder
return sortConfig.direction === 'asc' ? comparison : -comparison
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -535,20 +535,20 @@ const EntryNodeRow = memo(function EntryNodeRow({
)
})

interface TerminalLogListRowProps {
interface TerminalLogListDataRef {
rows: VisibleTerminalRow[]
selectedEntryId: string | null
onSelectEntry: (entry: ConsoleEntry) => void
expandedNodes: Set<string>
onToggleNode: (nodeId: string) => void
}

function TerminalLogListRow({
index,
style,
...props
}: RowComponentProps<TerminalLogListRowProps>) {
const { rows, selectedEntryId, onSelectEntry, expandedNodes, onToggleNode } = props
interface TerminalLogListRowProps {
dataRef: React.RefObject<TerminalLogListDataRef>
}

function TerminalLogListRow({ index, style, dataRef }: RowComponentProps<TerminalLogListRowProps>) {
const { rows, selectedEntryId, onSelectEntry, expandedNodes, onToggleNode } = dataRef.current
const row = rows[index]

if (row.rowType === 'separator') {
Expand Down Expand Up @@ -591,6 +591,8 @@ const TerminalLogsPane = memo(function TerminalLogsPane({
const containerRef = useRef<HTMLDivElement>(null)
const listRef = useListRef(null)
const [listHeight, setListHeight] = useState(400)
const prevRowCountRef = useRef(0)
const isNearBottomRef = useRef(true)

const rows = useMemo(
() => flattenVisibleExecutionRows(executionGroups, expandedNodes),
Expand All @@ -613,13 +615,59 @@ const TerminalLogsPane = memo(function TerminalLogsPane({
return () => resizeObserver.disconnect()
}, [])

const rowsRef = useRef(rows)
rowsRef.current = rows
useEffect(() => {
const tryAttach = () => {
const outerEl = listRef.current?.element
if (!outerEl) return false

const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = outerEl
const distanceFromBottom = scrollHeight - scrollTop - clientHeight
isNearBottomRef.current = distanceFromBottom < TERMINAL_CONFIG.LOG_ROW_HEIGHT_PX * 3
}

outerEl.addEventListener('scroll', handleScroll, { passive: true })
cleanupRef.current = () => outerEl.removeEventListener('scroll', handleScroll)
return true
}

const cleanupRef = { current: () => {} }

if (!tryAttach()) {
const frameId = requestAnimationFrame(() => tryAttach())
return () => {
cancelAnimationFrame(frameId)
cleanupRef.current()
}
}

return () => cleanupRef.current()
}, [listRef])

useEffect(() => {
const newCount = rows.length
const prevCount = prevRowCountRef.current
prevRowCountRef.current = newCount

if (newCount <= prevCount || newCount === 0) return
if (!isNearBottomRef.current) return

listRef.current?.scrollToRow({ index: newCount - 1, align: 'end' })
}, [rows.length, listRef])

const dataRef = useRef<TerminalLogListDataRef>({
rows,
selectedEntryId,
onSelectEntry,
expandedNodes,
onToggleNode,
})
dataRef.current = { rows, selectedEntryId, onSelectEntry, expandedNodes, onToggleNode }

useEffect(() => {
if (!selectedEntryId) return

const currentRows = rowsRef.current
const currentRows = dataRef.current.rows
const rowIndex = currentRows.findIndex(
(row) => row.rowType === 'node' && row.node?.entry.id === selectedEntryId
)
Expand All @@ -629,27 +677,19 @@ const TerminalLogsPane = memo(function TerminalLogsPane({
}
}, [selectedEntryId, listRef])

const rowProps = useMemo<TerminalLogListRowProps>(
() => ({
rows,
selectedEntryId,
onSelectEntry,
expandedNodes,
onToggleNode,
}),
[rows, selectedEntryId, onSelectEntry, expandedNodes, onToggleNode]
)
const rowProps = useMemo<TerminalLogListRowProps>(() => ({ dataRef }), [dataRef])

return (
<div ref={containerRef} className='h-full'>
<div ref={containerRef} className='h-full bg-[var(--bg-primary)]' style={{ contain: 'strict' }}>
<List
className='bg-[var(--bg-primary)]'
listRef={listRef}
defaultHeight={listHeight}
rowCount={rows.length}
rowHeight={TERMINAL_CONFIG.LOG_ROW_HEIGHT_PX}
rowComponent={TerminalLogListRow}
rowProps={rowProps}
overscanCount={8}
overscanCount={30}
/>
</div>
)
Expand Down Expand Up @@ -697,6 +737,7 @@ export const Terminal = memo(function Terminal() {
const [showCopySuccess, setShowCopySuccess] = useState(false)
const [showInput, setShowInput] = useState(false)
const [autoSelectEnabled, setAutoSelectEnabled] = useState(true)
const autoSelectExecutionIdRef = useRef<string | null>(null)
const [mainOptionsOpen, setMainOptionsOpen] = useState(false)

const [isTrainingEnvEnabled] = useState(() =>
Expand Down Expand Up @@ -773,12 +814,24 @@ export const Terminal = memo(function Terminal() {
return result
}, [executionGroups])

const prevAutoExpandKeyRef = useRef('')
const prevAutoExpandIdsRef = useRef<string[]>([])

const autoExpandNodeIds = useMemo(() => {
if (executionGroups.length === 0) {
return []
prevAutoExpandKeyRef.current = ''
prevAutoExpandIdsRef.current = []
return prevAutoExpandIdsRef.current
}

return collectExpandableNodeIds(executionGroups[0].entryTree)
const ids = collectExpandableNodeIds(executionGroups[0].entryTree)
const key = ids.join(',')
if (key === prevAutoExpandKeyRef.current) {
return prevAutoExpandIdsRef.current
}
prevAutoExpandKeyRef.current = key
prevAutoExpandIdsRef.current = ids
return ids
}, [executionGroups])

/**
Expand Down Expand Up @@ -877,17 +930,13 @@ export const Terminal = memo(function Terminal() {
useEffect(() => {
if (autoExpandNodeIds.length === 0) return

const rafId = requestAnimationFrame(() => {
setExpandedNodes((prev) => {
const hasAll = autoExpandNodeIds.every((id) => prev.has(id))
if (hasAll) return prev
const next = new Set(prev)
autoExpandNodeIds.forEach((id) => next.add(id))
return next
})
setExpandedNodes((prev) => {
const hasAll = autoExpandNodeIds.every((id) => prev.has(id))
if (hasAll) return prev
const next = new Set(prev)
autoExpandNodeIds.forEach((id) => next.add(id))
return next
})

return () => cancelAnimationFrame(rafId)
}, [autoExpandNodeIds])

/**
Expand Down Expand Up @@ -1095,12 +1144,21 @@ export const Terminal = memo(function Terminal() {
if (executionGroups.length === 0 || navigableEntries.length === 0) {
setAutoSelectEnabled(true)
setSelectedEntryId(null)
autoSelectExecutionIdRef.current = null
return
}

if (!autoSelectEnabled) return

const newestExecutionId = executionGroups[0].executionId
const isNewExecution = newestExecutionId !== autoSelectExecutionIdRef.current

if (isNewExecution) {
autoSelectExecutionIdRef.current = newestExecutionId
} else if (selectedEntryId !== null) {
return
}

let lastNavEntry: NavigableBlockEntry | null = null

for (const navEntry of navigableEntries) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,14 +366,24 @@ export function buildEntryTree(entries: ConsoleEntry[], idPrefix = ''): EntryNod
}
}

const nestedByContainerId = new Map<string, ConsoleEntry[]>()
for (const e of nestedIterationEntries) {
const parent = e.parentIterations?.[0]
if (!parent) continue
const key = parent.iterationContainerId
const list = nestedByContainerId.get(key)
if (list) {
list.push(e)
} else {
nestedByContainerId.set(key, [e])
}
}

const subflowNodes: EntryNode[] = []
for (const subflowGroup of subflowGroups.values()) {
const { iterationType, iterationContainerId, groups: iterationGroups } = subflowGroup

const nestedForThisSubflow = nestedIterationEntries.filter((e) => {
const parent = e.parentIterations?.[0]
return parent && parent.iterationContainerId === iterationContainerId
})
const nestedForThisSubflow = nestedByContainerId.get(iterationContainerId) ?? []

const allDirectBlocks = iterationGroups.flatMap((g) => g.blocks)
const allRelevantBlocks = [...allDirectBlocks, ...nestedForThisSubflow]
Expand Down Expand Up @@ -406,12 +416,21 @@ export function buildEntryTree(entries: ConsoleEntry[], idPrefix = ''): EntryNod
iterationContainerId,
}

const nestedByIteration = new Map<number, ConsoleEntry[]>()
for (const e of nestedForThisSubflow) {
const iterNum = e.parentIterations?.[0]?.iterationCurrent
if (iterNum === undefined) continue
const list = nestedByIteration.get(iterNum)
if (list) {
list.push(e)
} else {
nestedByIteration.set(iterNum, [e])
}
}

const iterationNodes: EntryNode[] = iterationGroups
.map((iterGroup): EntryNode | null => {
const matchingNestedEntries = nestedForThisSubflow.filter((e) => {
const parent = e.parentIterations?.[0]
return parent?.iterationCurrent === iterGroup.iterationCurrent
})
const matchingNestedEntries = nestedByIteration.get(iterGroup.iterationCurrent) ?? []

const strippedNestedEntries: ConsoleEntry[] = matchingNestedEntries.map((e) => ({
...e,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,7 @@ export function useBlockState(
const isDeletedBlock = !isShowingDiff && diffAnalysis?.deleted_blocks?.includes(blockId)

// Execution state
const isActiveBlock = useIsBlockActive(blockId)
const isActive = data.isActive || isActiveBlock
const isActive = useIsBlockActive(blockId)

return {
isEnabled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ export interface WorkflowBlockProps {
type: string
config: BlockConfig
name: string
isActive?: boolean
isPending?: boolean
isPreview?: boolean
/** Whether this block is selected in preview mode */
isPreviewSelected?: boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ export function shouldSkipBlockRender(
prevProps.id === nextProps.id &&
prevProps.data.type === nextProps.data.type &&
prevProps.data.name === nextProps.data.name &&
prevProps.data.isActive === nextProps.data.isActive &&
prevProps.data.isPending === nextProps.data.isPending &&
prevProps.data.isPreview === nextProps.data.isPreview &&
prevProps.data.isPreviewSelected === nextProps.data.isPreviewSelected &&
prevProps.data.config === nextProps.data.config &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { useReactivateSchedule, useScheduleInfo } from '@/hooks/queries/schedule
import { useSkills } from '@/hooks/queries/skills'
import { useTablesList } from '@/hooks/queries/tables'
import { useSelectorDisplayName } from '@/hooks/use-selector-display-name'
import { useIsBlockPending } from '@/stores/execution'
import { useVariablesStore } from '@/stores/panel'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
Expand Down Expand Up @@ -855,7 +856,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
data,
selected,
}: NodeProps<WorkflowBlockProps>) {
const { type, config, name, isPending, isSandbox } = data
const { type, config, name, isSandbox } = data

const contentRef = useRef<HTMLDivElement>(null)

Expand All @@ -873,7 +874,9 @@ export const WorkflowBlock = memo(function WorkflowBlock({
hasRing,
ringStyles,
runPathStatus,
} = useBlockVisual({ blockId: id, data, isPending, isSelected: selected })
} = useBlockVisual({ blockId: id, data, isSelected: selected })

const isPending = useIsBlockPending(id)

const currentBlock = currentWorkflow.getBlockById(id)

Expand Down
Loading
Loading