Skip to content

Commit af59234

Browse files
authored
v0.5.99: local dev improvements, live workflow logs in terminal
2 parents 0d86ea0 + 69ec70a commit af59234

File tree

22 files changed

+859
-128
lines changed

22 files changed

+859
-128
lines changed

apps/sim/app/api/function/execute/route.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ describe('Function Execute API Route', () => {
211211

212212
it.concurrent('should block SSRF attacks through secure fetch wrapper', async () => {
213213
expect(validateProxyUrl('http://169.254.169.254/latest/meta-data/').isValid).toBe(false)
214-
expect(validateProxyUrl('http://127.0.0.1:8080/admin').isValid).toBe(false)
214+
expect(validateProxyUrl('http://127.0.0.1:8080/admin').isValid).toBe(true)
215215
expect(validateProxyUrl('http://192.168.1.1/config').isValid).toBe(false)
216216
expect(validateProxyUrl('http://10.0.0.1/internal').isValid).toBe(false)
217217
})

apps/sim/app/api/workflows/[id]/execute/route.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { executeWorkflowJob, type WorkflowExecutionPayload } from '@/background/
3838
import { normalizeName } from '@/executor/constants'
3939
import { ExecutionSnapshot } from '@/executor/execution/snapshot'
4040
import type {
41+
ChildWorkflowContext,
4142
ExecutionMetadata,
4243
IterationContext,
4344
SerializableExecutionState,
@@ -742,7 +743,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
742743
blockName: string,
743744
blockType: string,
744745
executionOrder: number,
745-
iterationContext?: IterationContext
746+
iterationContext?: IterationContext,
747+
childWorkflowContext?: ChildWorkflowContext
746748
) => {
747749
logger.info(`[${requestId}] 🔷 onBlockStart called:`, { blockId, blockName, blockType })
748750
sendEvent({
@@ -761,6 +763,10 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
761763
iterationType: iterationContext.iterationType,
762764
iterationContainerId: iterationContext.iterationContainerId,
763765
}),
766+
...(childWorkflowContext && {
767+
childWorkflowBlockId: childWorkflowContext.parentBlockId,
768+
childWorkflowName: childWorkflowContext.workflowName,
769+
}),
764770
},
765771
})
766772
}
@@ -770,9 +776,20 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
770776
blockName: string,
771777
blockType: string,
772778
callbackData: any,
773-
iterationContext?: IterationContext
779+
iterationContext?: IterationContext,
780+
childWorkflowContext?: ChildWorkflowContext
774781
) => {
775782
const hasError = callbackData.output?.error
783+
const childWorkflowData = childWorkflowContext
784+
? {
785+
childWorkflowBlockId: childWorkflowContext.parentBlockId,
786+
childWorkflowName: childWorkflowContext.workflowName,
787+
}
788+
: {}
789+
790+
const instanceData = callbackData.childWorkflowInstanceId
791+
? { childWorkflowInstanceId: callbackData.childWorkflowInstanceId }
792+
: {}
776793

777794
if (hasError) {
778795
logger.info(`[${requestId}] ✗ onBlockComplete (error) called:`, {
@@ -802,6 +819,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
802819
iterationType: iterationContext.iterationType,
803820
iterationContainerId: iterationContext.iterationContainerId,
804821
}),
822+
...childWorkflowData,
823+
...instanceData,
805824
},
806825
})
807826
} else {
@@ -831,6 +850,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
831850
iterationType: iterationContext.iterationType,
832851
iterationContainerId: iterationContext.iterationContainerId,
833852
}),
853+
...childWorkflowData,
854+
...instanceData,
834855
},
835856
})
836857
}
@@ -898,12 +919,34 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
898919
selectedOutputs
899920
)
900921

922+
const onChildWorkflowInstanceReady = (
923+
blockId: string,
924+
childWorkflowInstanceId: string,
925+
iterationContext?: IterationContext
926+
) => {
927+
sendEvent({
928+
type: 'block:childWorkflowStarted',
929+
timestamp: new Date().toISOString(),
930+
executionId,
931+
workflowId,
932+
data: {
933+
blockId,
934+
childWorkflowInstanceId,
935+
...(iterationContext && {
936+
iterationCurrent: iterationContext.iterationCurrent,
937+
iterationContainerId: iterationContext.iterationContainerId,
938+
}),
939+
},
940+
})
941+
}
942+
901943
const result = await executeWorkflowCore({
902944
snapshot,
903945
callbacks: {
904946
onBlockStart,
905947
onBlockComplete,
906948
onStream,
949+
onChildWorkflowInstanceReady,
907950
},
908951
loggingSession,
909952
abortSignal: timeoutController.signal,

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx

Lines changed: 148 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks'
4242
import { ROW_STYLES } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types'
4343
import {
44+
collectExpandableNodeIds,
4445
type EntryNode,
4546
type ExecutionGroup,
4647
flattenBlockEntriesOnly,
@@ -67,6 +68,21 @@ const MIN_HEIGHT = TERMINAL_HEIGHT.MIN
6768
const DEFAULT_EXPANDED_HEIGHT = TERMINAL_HEIGHT.DEFAULT
6869
const MIN_OUTPUT_PANEL_WIDTH_PX = OUTPUT_PANEL_WIDTH.MIN
6970

71+
/** Returns true if any node in the subtree has an error */
72+
function hasErrorInTree(nodes: EntryNode[]): boolean {
73+
return nodes.some((n) => Boolean(n.entry.error) || hasErrorInTree(n.children))
74+
}
75+
76+
/** Returns true if any node in the subtree is currently running */
77+
function hasRunningInTree(nodes: EntryNode[]): boolean {
78+
return nodes.some((n) => Boolean(n.entry.isRunning) || hasRunningInTree(n.children))
79+
}
80+
81+
/** Returns true if any node in the subtree was canceled */
82+
function hasCanceledInTree(nodes: EntryNode[]): boolean {
83+
return nodes.some((n) => Boolean(n.entry.isCanceled) || hasCanceledInTree(n.children))
84+
}
85+
7086
/**
7187
* Block row component for displaying actual block entries
7288
*/
@@ -338,6 +354,122 @@ const SubflowNodeRow = memo(function SubflowNodeRow({
338354
)
339355
})
340356

357+
/**
358+
* Workflow node component - shows workflow block header with nested child blocks
359+
*/
360+
const WorkflowNodeRow = memo(function WorkflowNodeRow({
361+
node,
362+
selectedEntryId,
363+
onSelectEntry,
364+
expandedNodes,
365+
onToggleNode,
366+
}: {
367+
node: EntryNode
368+
selectedEntryId: string | null
369+
onSelectEntry: (entry: ConsoleEntry) => void
370+
expandedNodes: Set<string>
371+
onToggleNode: (nodeId: string) => void
372+
}) {
373+
const { entry, children } = node
374+
const BlockIcon = getBlockIcon(entry.blockType)
375+
const bgColor = getBlockColor(entry.blockType)
376+
const nodeId = entry.id
377+
const isExpanded = expandedNodes.has(nodeId)
378+
const hasChildren = children.length > 0
379+
const isSelected = selectedEntryId === entry.id
380+
381+
const hasError = useMemo(
382+
() => Boolean(entry.error) || hasErrorInTree(children),
383+
[entry.error, children]
384+
)
385+
const hasRunningDescendant = useMemo(
386+
() => Boolean(entry.isRunning) || hasRunningInTree(children),
387+
[entry.isRunning, children]
388+
)
389+
const hasCanceledDescendant = useMemo(
390+
() => (Boolean(entry.isCanceled) || hasCanceledInTree(children)) && !hasRunningDescendant,
391+
[entry.isCanceled, children, hasRunningDescendant]
392+
)
393+
394+
return (
395+
<div className='flex min-w-0 flex-col'>
396+
{/* Workflow Block Header */}
397+
<div
398+
className={clsx(
399+
ROW_STYLES.base,
400+
'h-[26px]',
401+
isSelected ? ROW_STYLES.selected : ROW_STYLES.hover
402+
)}
403+
onClick={(e) => {
404+
e.stopPropagation()
405+
if (!isSelected) onSelectEntry(entry)
406+
if (hasChildren) onToggleNode(nodeId)
407+
}}
408+
>
409+
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
410+
<div
411+
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded-[4px]'
412+
style={{ background: bgColor }}
413+
>
414+
{BlockIcon && <BlockIcon className='h-[9px] w-[9px] text-white' />}
415+
</div>
416+
<span
417+
className={clsx(
418+
'min-w-0 truncate font-medium text-[13px]',
419+
hasError
420+
? 'text-[var(--text-error)]'
421+
: isSelected || isExpanded
422+
? 'text-[var(--text-primary)]'
423+
: 'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]'
424+
)}
425+
>
426+
{entry.blockName}
427+
</span>
428+
{hasChildren && (
429+
<ChevronDown
430+
className={clsx(
431+
'h-[8px] w-[8px] flex-shrink-0 text-[var(--text-tertiary)] transition-transform duration-100 group-hover:text-[var(--text-primary)]',
432+
!isExpanded && '-rotate-90'
433+
)}
434+
/>
435+
)}
436+
</div>
437+
<span
438+
className={clsx(
439+
'flex-shrink-0 font-medium text-[13px]',
440+
!hasRunningDescendant &&
441+
(hasCanceledDescendant
442+
? 'text-[var(--text-secondary)]'
443+
: 'text-[var(--text-tertiary)]')
444+
)}
445+
>
446+
<StatusDisplay
447+
isRunning={hasRunningDescendant}
448+
isCanceled={hasCanceledDescendant}
449+
formattedDuration={formatDuration(entry.durationMs, { precision: 2 }) ?? '-'}
450+
/>
451+
</span>
452+
</div>
453+
454+
{/* Nested Child Blocks — rendered through EntryNodeRow for full loop/parallel support */}
455+
{isExpanded && hasChildren && (
456+
<div className={ROW_STYLES.nested}>
457+
{children.map((child) => (
458+
<EntryNodeRow
459+
key={child.entry.id}
460+
node={child}
461+
selectedEntryId={selectedEntryId}
462+
onSelectEntry={onSelectEntry}
463+
expandedNodes={expandedNodes}
464+
onToggleNode={onToggleNode}
465+
/>
466+
))}
467+
</div>
468+
)}
469+
</div>
470+
)
471+
})
472+
341473
/**
342474
* Entry node component - dispatches to appropriate component based on node type
343475
*/
@@ -368,6 +500,18 @@ const EntryNodeRow = memo(function EntryNodeRow({
368500
)
369501
}
370502

503+
if (nodeType === 'workflow') {
504+
return (
505+
<WorkflowNodeRow
506+
node={node}
507+
selectedEntryId={selectedEntryId}
508+
onSelectEntry={onSelectEntry}
509+
expandedNodes={expandedNodes}
510+
onToggleNode={onToggleNode}
511+
/>
512+
)
513+
}
514+
371515
if (nodeType === 'iteration') {
372516
return (
373517
<IterationNodeRow
@@ -659,27 +803,15 @@ export const Terminal = memo(function Terminal() {
659803
])
660804

661805
/**
662-
* Auto-expand subflows and iterations when new entries arrive.
806+
* Auto-expand subflows, iterations, and workflow nodes when new entries arrive.
807+
* Recursively walks the full tree so nested nodes (e.g. a workflow block inside
808+
* a loop iteration) are also expanded automatically.
663809
* This always runs regardless of autoSelectEnabled - new runs should always be visible.
664810
*/
665811
useEffect(() => {
666812
if (executionGroups.length === 0) return
667813

668-
const newestExec = executionGroups[0]
669-
670-
// Collect all node IDs that should be expanded (subflows and their iterations)
671-
const nodeIdsToExpand: string[] = []
672-
for (const node of newestExec.entryTree) {
673-
if (node.nodeType === 'subflow' && node.children.length > 0) {
674-
nodeIdsToExpand.push(node.entry.id)
675-
// Also expand all iteration children
676-
for (const iterNode of node.children) {
677-
if (iterNode.nodeType === 'iteration') {
678-
nodeIdsToExpand.push(iterNode.entry.id)
679-
}
680-
}
681-
}
682-
}
814+
const nodeIdsToExpand = collectExpandableNodeIds(executionGroups[0].entryTree)
683815

684816
if (nodeIdsToExpand.length > 0) {
685817
setExpandedNodes((prev) => {

0 commit comments

Comments
 (0)