@@ -10,6 +10,8 @@ import { extractReferencePrefixes } from '@/lib/workflows/sanitization/reference
1010import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components'
1111import { getBlock } from '@/blocks'
1212import type { BlockConfig , SubBlockConfig } from '@/blocks/types'
13+ import { normalizeName } from '@/executor/constants'
14+ import { navigatePath } from '@/executor/variables/resolvers/reference'
1315import type { BlockState } from '@/stores/workflows/workflow/types'
1416
1517/**
@@ -82,26 +84,16 @@ function formatValueAsJson(value: unknown): string {
8284interface ResolvedVariable {
8385 tag : string
8486 value : string
87+ blockName : string
88+ blockType : string
89+ fieldPath : string
8590}
8691
87- /**
88- * Navigate a path in an object (e.g., ['response', 'data'] in { response: { data: 'value' } })
89- */
90- function navigatePath ( obj : unknown , path : string [ ] ) : unknown {
91- let current : unknown = obj
92- for ( const part of path ) {
93- if ( current === null || current === undefined ) return undefined
94- if ( typeof current !== 'object' ) return undefined
95- current = ( current as Record < string , unknown > ) [ part ]
96- }
97- return current
98- }
99-
100- /**
101- * Normalize a block name for comparison (lowercase, remove spaces)
102- */
103- function normalizeName ( name : string ) : string {
104- return name . toLowerCase ( ) . replace ( / \s + / g, '' )
92+ interface ResolvedConnection {
93+ blockId : string
94+ blockName : string
95+ blockType : string
96+ fields : Array < { path : string ; value : string ; tag : string } >
10597}
10698
10799/**
@@ -117,8 +109,11 @@ function extractAllReferencesFromSubBlocks(subBlockValues: Record<string, unknow
117109 } else if ( Array . isArray ( value ) ) {
118110 value . forEach ( processValue )
119111 } else if ( value && typeof value === 'object' ) {
120- // Process ALL properties of objects to find nested references
121- Object . values ( value ) . forEach ( processValue )
112+ if ( 'value' in value ) {
113+ processValue ( ( value as { value : unknown } ) . value )
114+ } else {
115+ Object . values ( value ) . forEach ( processValue )
116+ }
122117 }
123118 }
124119
@@ -222,10 +217,26 @@ function ExecutionDataSection({ title, data, isError = false }: ExecutionDataSec
222217/**
223218 * Section showing resolved variable references - styled like the connections section in editor
224219 */
225- function ResolvedConnectionsSection ( { variables } : { variables : ResolvedVariable [ ] } ) {
220+ function ResolvedConnectionsSection ( { connections } : { connections : ResolvedConnection [ ] } ) {
226221 const [ isCollapsed , setIsCollapsed ] = useState ( false )
222+ const [ expandedBlocks , setExpandedBlocks ] = useState < Set < string > > ( ( ) => {
223+ // Start with all blocks expanded
224+ return new Set ( connections . map ( ( c ) => c . blockId ) )
225+ } )
226+
227+ if ( connections . length === 0 ) return null
227228
228- if ( variables . length === 0 ) return null
229+ const toggleBlock = ( blockId : string ) => {
230+ setExpandedBlocks ( ( prev ) => {
231+ const next = new Set ( prev )
232+ if ( next . has ( blockId ) ) {
233+ next . delete ( blockId )
234+ } else {
235+ next . add ( blockId )
236+ }
237+ return next
238+ } )
239+ }
229240
230241 return (
231242 < div className = 'flex flex-shrink-0 flex-col border-[var(--border)] border-t' >
@@ -249,25 +260,86 @@ function ResolvedConnectionsSection({ variables }: { variables: ResolvedVariable
249260 < div className = 'font-medium text-[13px] text-[var(--text-primary)]' > Connections</ div >
250261 </ div >
251262
252- { /* Content */ }
263+ { /* Content - styled like ConnectionBlocks */ }
253264 { ! isCollapsed && (
254265 < div className = 'space-y-[2px] px-[6px] pb-[8px]' >
255- { variables . map ( ( variable ) => (
256- < div key = { variable . tag } className = 'mb-[2px] last:mb-0' >
257- < div
258- className = { clsx (
259- 'group flex h-[26px] items-center gap-[8px] rounded-[8px] px-[6px] text-[14px]'
266+ { connections . map ( ( connection ) => {
267+ const blockConfig = getBlock ( connection . blockType )
268+ const Icon = blockConfig ?. icon
269+ const bgColor = blockConfig ?. bgColor || '#6B7280'
270+ const isExpanded = expandedBlocks . has ( connection . blockId )
271+ const hasFields = connection . fields . length > 0
272+
273+ return (
274+ < div key = { connection . blockId } className = 'mb-[2px] last:mb-0' >
275+ { /* Block header - styled like ConnectionItem */ }
276+ < div
277+ className = { clsx (
278+ 'group flex h-[26px] items-center gap-[8px] rounded-[8px] px-[6px] text-[14px] hover:bg-[var(--surface-6)] dark:hover:bg-[var(--surface-5)]' ,
279+ hasFields && 'cursor-pointer'
280+ ) }
281+ onClick = { ( ) => hasFields && toggleBlock ( connection . blockId ) }
282+ >
283+ < div
284+ className = 'relative flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
285+ style = { { background : bgColor } }
286+ >
287+ { Icon && (
288+ < Icon
289+ className = { clsx (
290+ 'text-white transition-transform duration-200' ,
291+ hasFields && 'group-hover:scale-110' ,
292+ '!h-[9px] !w-[9px]'
293+ ) }
294+ />
295+ ) }
296+ </ div >
297+ < span
298+ className = { clsx (
299+ 'truncate font-medium' ,
300+ 'text-[var(--text-secondary)] group-hover:text-[var(--text-primary)]'
301+ ) }
302+ >
303+ { connection . blockName }
304+ </ span >
305+ { hasFields && (
306+ < ChevronDownIcon
307+ className = { clsx (
308+ 'h-3.5 w-3.5 flex-shrink-0 transition-transform duration-100' ,
309+ 'text-[var(--text-secondary)] group-hover:text-[var(--text-primary)]' ,
310+ isExpanded && 'rotate-180'
311+ ) }
312+ />
313+ ) }
314+ </ div >
315+
316+ { /* Fields - styled like FieldItem but showing resolved values */ }
317+ { isExpanded && hasFields && (
318+ < div className = 'relative mt-[2px] ml-[12px] space-y-[2px] pl-[10px]' >
319+ < div className = 'pointer-events-none absolute top-[4px] bottom-[4px] left-0 w-px bg-[var(--border)]' />
320+ { connection . fields . map ( ( field ) => (
321+ < div
322+ key = { field . tag }
323+ className = 'group flex h-[26px] items-center gap-[8px] rounded-[8px] px-[6px] text-[14px] hover:bg-[var(--surface-6)] dark:hover:bg-[var(--surface-5)]'
324+ >
325+ < span
326+ className = { clsx (
327+ 'flex-shrink-0 font-medium' ,
328+ 'text-[var(--text-secondary)] group-hover:text-[var(--text-primary)]'
329+ ) }
330+ >
331+ { field . path }
332+ </ span >
333+ < span className = 'min-w-0 flex-1 truncate text-[var(--text-tertiary)]' >
334+ { field . value }
335+ </ span >
336+ </ div >
337+ ) ) }
338+ </ div >
260339 ) }
261- >
262- < span className = { clsx ( 'truncate font-medium' , 'text-[var(--brand-secondary)]' ) } >
263- { variable . tag }
264- </ span >
265- < span className = { clsx ( 'min-w-0 flex-1 truncate' , 'text-[var(--text-secondary)]' ) } >
266- { variable . value }
267- </ span >
268340 </ div >
269- </ div >
270- ) ) }
341+ )
342+ } ) }
271343 </ div >
272344 ) }
273345 </ div >
@@ -361,27 +433,55 @@ function BlockDetailsSidebarContent({
361433 }
362434 } , [ allBlockExecutions , workflowBlocks , blockNameToId ] )
363435
364- const resolvedVariables = useMemo ( ( ) : ResolvedVariable [ ] => {
365- const allRefs = extractAllReferencesFromSubBlocks ( subBlockValues )
366- if ( allRefs . length === 0 ) return [ ]
436+ // Group resolved variables by source block for display
437+ const resolvedConnections = useMemo ( ( ) : ResolvedConnection [ ] => {
438+ if ( ! allBlockExecutions || ! workflowBlocks ) return [ ]
367439
368- const results : ResolvedVariable [ ] = [ ]
440+ const allRefs = extractAllReferencesFromSubBlocks ( subBlockValues )
369441 const seen = new Set < string > ( )
442+ const blockMap = new Map < string , ResolvedConnection > ( )
370443
371444 for ( const ref of allRefs ) {
372445 if ( seen . has ( ref ) ) continue
373- seen . add ( ref )
374446
375- // Try to resolve from block executions if available
447+ // Parse reference: <blockName.path.to.value>
448+ const inner = ref . slice ( 1 , - 1 )
449+ const parts = inner . split ( '.' )
450+ if ( parts . length < 1 ) continue
451+
452+ const [ blockName , ...pathParts ] = parts
453+ const normalizedBlockName = normalizeName ( blockName )
454+ const blockId = blockNameToId . get ( normalizedBlockName )
455+ if ( ! blockId ) continue
456+
457+ const sourceBlock = workflowBlocks [ blockId ]
458+ if ( ! sourceBlock ) continue
459+
376460 const resolvedValue = resolveReference ( ref )
377- results . push ( {
461+ if ( resolvedValue === undefined ) continue
462+
463+ seen . add ( ref )
464+
465+ // Get or create block entry
466+ if ( ! blockMap . has ( blockId ) ) {
467+ blockMap . set ( blockId , {
468+ blockId,
469+ blockName : sourceBlock . name || blockName ,
470+ blockType : sourceBlock . type ,
471+ fields : [ ] ,
472+ } )
473+ }
474+
475+ const connection = blockMap . get ( blockId ) !
476+ connection . fields . push ( {
477+ path : pathParts . join ( '.' ) || 'output' ,
478+ value : formatInlineValue ( resolvedValue ) ,
378479 tag : ref ,
379- value : resolvedValue !== undefined ? formatInlineValue ( resolvedValue ) : '—' ,
380480 } )
381481 }
382482
383- return results
384- } , [ subBlockValues , resolveReference ] )
483+ return Array . from ( blockMap . values ( ) )
484+ } , [ subBlockValues , allBlockExecutions , workflowBlocks , blockNameToId , resolveReference ] )
385485
386486 if ( ! blockConfig ) {
387487 return (
@@ -559,12 +659,12 @@ function BlockDetailsSidebarContent({
559659 </ div >
560660 ) }
561661 </ div >
562-
563- { /* Resolved Variables Section - Shows what variable references resolved to */ }
564- { resolvedVariables . length > 0 && (
565- < ResolvedConnectionsSection variables = { resolvedVariables } />
566- ) }
567662 </ div >
663+
664+ { /* Resolved Variables Section - Pinned at bottom, outside scrollable area */ }
665+ { resolvedConnections . length > 0 && (
666+ < ResolvedConnectionsSection connections = { resolvedConnections } />
667+ ) }
568668 </ div >
569669 )
570670}
0 commit comments