fix(env-vars): remove regex parsing from table subblock, add formatDisplayText to various subblocks that didn't have it (#1582)

This commit is contained in:
Waleed
2025-10-08 12:57:37 -07:00
committed by waleed
parent c04eb01aed
commit 88d2e7b97b
10 changed files with 105 additions and 209 deletions

View File

@@ -9,6 +9,7 @@ import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
import { MAX_TAG_SLOTS } from '@/lib/knowledge/consts'
import { cn } from '@/lib/utils'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import type { SubBlockConfig } from '@/blocks/types'
import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions'
import { useTagSelection } from '@/hooks/use-tag-selection'
@@ -40,6 +41,7 @@ export function DocumentTagEntry({
isConnecting = false,
}: DocumentTagEntryProps) {
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlock.id)
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
// Get the knowledge base ID from other sub-blocks
const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId')
@@ -301,7 +303,12 @@ export function DocumentTagEntry({
)}
/>
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'>
<div className='whitespace-pre'>{formatDisplayText(cellValue)}</div>
<div className='whitespace-pre'>
{formatDisplayText(cellValue, {
accessiblePrefixes,
highlightAll: !accessiblePrefixes,
})}
</div>
</div>
{showDropdown && availableTagDefinitions.length > 0 && (
<div className='absolute top-full left-0 z-[100] mt-1 w-full'>
@@ -389,7 +396,10 @@ export function DocumentTagEntry({
/>
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'>
<div className='whitespace-pre text-muted-foreground'>
{formatDisplayText(cellValue)}
{formatDisplayText(cellValue, {
accessiblePrefixes,
highlightAll: !accessiblePrefixes,
})}
</div>
</div>
{showTypeDropdown && !isReadOnly && (
@@ -469,7 +479,12 @@ export function DocumentTagEntry({
className='w-full border-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0'
/>
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'>
<div className='whitespace-pre'>{formatDisplayText(cellValue)}</div>
<div className='whitespace-pre'>
{formatDisplayText(cellValue, {
accessiblePrefixes,
highlightAll: !accessiblePrefixes,
})}
</div>
</div>
</div>
</td>

View File

@@ -239,7 +239,12 @@ export function KnowledgeTagFilters({
onBlur={handleBlur}
/>
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'>
<div className='whitespace-pre'>{formatDisplayText(cellValue || 'Select tag')}</div>
<div className='whitespace-pre'>
{formatDisplayText(cellValue || 'Select tag', {
accessiblePrefixes,
highlightAll: !accessiblePrefixes,
})}
</div>
</div>
{showDropdown && tagDefinitions.length > 0 && (
<div className='absolute top-full left-0 z-[100] mt-1 w-full'>

View File

@@ -1,5 +1,6 @@
import { useCallback } from 'react'
import { useParams } from 'next/navigation'
import { formatDisplayText } from '@/components/ui/formatted-text'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
@@ -14,6 +15,7 @@ import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import { cn } from '@/lib/utils'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import { useMcpTools } from '@/hooks/use-mcp-tools'
import { formatParameterLabel } from '@/tools/params'
@@ -37,6 +39,7 @@ export function McpDynamicArgs({
const { mcpTools } = useMcpTools(workspaceId)
const [selectedTool] = useSubBlockValue(blockId, 'tool')
const [toolArgs, setToolArgs] = useSubBlockValue(blockId, subBlockId)
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
const selectedToolConfig = mcpTools.find((tool) => tool.id === selectedTool)
const toolSchema = selectedToolConfig?.inputSchema
@@ -180,7 +183,7 @@ export function McpDynamicArgs({
case 'long-input':
return (
<div key={`${paramName}-long`}>
<div key={`${paramName}-long`} className='relative'>
<Textarea
value={value || ''}
onChange={(e) => updateParameter(paramName, e.target.value, paramSchema)}
@@ -192,8 +195,14 @@ export function McpDynamicArgs({
}
disabled={disabled}
rows={4}
className='min-h-[80px] resize-none'
className='min-h-[80px] resize-none text-transparent caret-foreground'
/>
<div className='pointer-events-none absolute inset-0 overflow-auto whitespace-pre-wrap break-words p-3 text-sm'>
{formatDisplayText(value || '', {
accessiblePrefixes,
highlightAll: !accessiblePrefixes,
})}
</div>
</div>
)
@@ -203,9 +212,10 @@ export function McpDynamicArgs({
paramName.toLowerCase().includes('password') ||
paramName.toLowerCase().includes('token')
const isNumeric = paramSchema.type === 'number' || paramSchema.type === 'integer'
const isTextInput = !isPassword && !isNumeric
return (
<div key={`${paramName}-short`}>
<div key={`${paramName}-short`} className={isTextInput ? 'relative' : ''}>
<Input
type={isPassword ? 'password' : isNumeric ? 'number' : 'text'}
value={value || ''}
@@ -231,7 +241,18 @@ export function McpDynamicArgs({
`Enter ${formatParameterLabel(paramName).toLowerCase()}`
}
disabled={disabled}
className={isTextInput ? 'text-transparent caret-foreground' : ''}
/>
{isTextInput && (
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'>
<div className='whitespace-pre'>
{formatDisplayText(value?.toString() || '', {
accessiblePrefixes,
highlightAll: !accessiblePrefixes,
})}
</div>
</div>
)}
</div>
)
}

View File

@@ -8,6 +8,7 @@ import { Input } from '@/components/ui/input'
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
import { cn } from '@/lib/utils'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
interface TableProps {
blockId: string
@@ -34,6 +35,7 @@ export function Table({
const params = useParams()
const workspaceId = params.workspaceId as string
const [storeValue, setStoreValue] = useSubBlockValue<TableRow[]>(blockId, subBlockId)
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
@@ -240,7 +242,12 @@ export function Table({
data-overlay={cellKey}
className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'
>
<div className='whitespace-pre'>{formatDisplayText(cellValue)}</div>
<div className='whitespace-pre'>
{formatDisplayText(cellValue, {
accessiblePrefixes,
highlightAll: !accessiblePrefixes,
})}
</div>
</div>
</div>
</td>

View File

@@ -10,6 +10,7 @@ import {
CommandItem,
CommandList,
} from '@/components/ui/command'
import { formatDisplayText } from '@/components/ui/formatted-text'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
@@ -23,9 +24,11 @@ import {
import { Switch } from '@/components/ui/switch'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import type { TriggerConfig } from '@/triggers/types'
interface TriggerConfigSectionProps {
blockId: string
triggerDef: TriggerConfig
config: Record<string, any>
onChange: (fieldId: string, value: any) => void
@@ -34,6 +37,7 @@ interface TriggerConfigSectionProps {
}
export function TriggerConfigSection({
blockId,
triggerDef,
config,
onChange,
@@ -42,6 +46,7 @@ export function TriggerConfigSection({
}: TriggerConfigSectionProps) {
const [showSecrets, setShowSecrets] = useState<Record<string, boolean>>({})
const [copied, setCopied] = useState<string | null>(null)
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
const copyToClipboard = (text: string, type: string) => {
navigator.clipboard.writeText(text)
@@ -258,9 +263,20 @@ export function TriggerConfigSection({
className={cn(
'h-9 rounded-[8px]',
isSecret ? 'pr-32' : '',
'focus-visible:ring-2 focus-visible:ring-primary/20'
'focus-visible:ring-2 focus-visible:ring-primary/20',
!isSecret && 'text-transparent caret-foreground'
)}
/>
{!isSecret && (
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'>
<div className='whitespace-pre'>
{formatDisplayText(value?.toString() || '', {
accessiblePrefixes,
highlightAll: !accessiblePrefixes,
})}
</div>
</div>
)}
{isSecret && (
<div className='absolute top-0.5 right-0.5 flex h-8 items-center gap-1 pr-1'>
<Button

View File

@@ -467,6 +467,7 @@ export function TriggerModal({
)}
<TriggerConfigSection
blockId={blockId}
triggerDef={triggerDef}
config={config}
onChange={handleConfigChange}

View File

@@ -165,7 +165,7 @@ describe('ConditionBlockHandler', () => {
mockContext,
mockBlock
)
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('context.value > 5', true)
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('context.value > 5')
expect(result).toEqual(expectedOutput)
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('cond1')
})
@@ -205,7 +205,7 @@ describe('ConditionBlockHandler', () => {
mockContext,
mockBlock
)
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('context.value < 0', true)
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('context.value < 0')
expect(result).toEqual(expectedOutput)
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('else1')
})
@@ -241,7 +241,7 @@ describe('ConditionBlockHandler', () => {
mockContext,
mockBlock
)
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('10 > 5', true)
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('10 > 5')
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('cond1')
})
@@ -268,7 +268,7 @@ describe('ConditionBlockHandler', () => {
mockContext,
mockBlock
)
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('"john" !== null', true)
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('"john" !== null')
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('cond1')
})
@@ -295,7 +295,7 @@ describe('ConditionBlockHandler', () => {
mockContext,
mockBlock
)
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('{{POOP}} === "hi"', true)
expect(mockResolver.resolveEnvVariables).toHaveBeenCalledWith('{{POOP}} === "hi"')
expect(mockContext.decisions.condition.get(mockBlock.id)).toBe('cond1')
})

View File

@@ -109,7 +109,7 @@ export class ConditionBlockHandler implements BlockHandler {
// Use full resolution pipeline: variables -> block references -> env vars
const resolvedVars = this.resolver.resolveVariableReferences(conditionValueString, block)
const resolvedRefs = this.resolver.resolveBlockReferences(resolvedVars, context, block)
resolvedConditionValue = this.resolver.resolveEnvVariables(resolvedRefs, true)
resolvedConditionValue = this.resolver.resolveEnvVariables(resolvedRefs)
logger.info(
`Resolved condition "${condition.title}" (${condition.id}): from "${conditionValueString}" to "${resolvedConditionValue}"`
)

View File

@@ -432,7 +432,7 @@ describe('InputResolver', () => {
expect(result.apiKey).toBe('test-api-key')
expect(result.url).toBe('https://example.com?key=test-api-key')
expect(result.regularParam).toBe('Base URL is: {{BASE_URL}}')
expect(result.regularParam).toBe('Base URL is: https://api.example.com')
})
it('should resolve explicit environment variables', () => {
@@ -458,7 +458,7 @@ describe('InputResolver', () => {
expect(result.explicitEnv).toBe('https://api.example.com')
})
it('should not resolve environment variables in regular contexts', () => {
it('should resolve environment variables in all contexts', () => {
const block: SerializedBlock = {
id: 'test-block',
metadata: { id: 'generic', name: 'Test Block' },
@@ -478,7 +478,7 @@ describe('InputResolver', () => {
const result = resolver.resolveInputs(block, mockContext)
expect(result.regularParam).toBe('Value with {{API_KEY}} embedded')
expect(result.regularParam).toBe('Value with test-api-key embedded')
})
})

View File

@@ -313,7 +313,7 @@ export class InputResolver {
}
// Handle objects and arrays recursively
else if (typeof value === 'object') {
result[key] = this.processObjectValue(value, key, context, block)
result[key] = this.processObjectValue(value, context, block)
}
// Pass through other value types
else {
@@ -684,7 +684,7 @@ export class InputResolver {
}
// Standard block reference resolution with connection validation
const validation = this.validateBlockReference(blockRef, currentBlock.id, context)
const validation = this.validateBlockReference(blockRef, currentBlock.id)
if (!validation.isValid) {
throw new Error(validation.errorMessage!)
@@ -845,142 +845,42 @@ export class InputResolver {
return resolvedValue
}
/**
* Validates if a match with < and > is actually a variable reference.
* Valid variable references must:
* - Have no space after the opening <
* - Contain a dot (.)
* - Have no spaces until the closing >
* - Not be comparison operators or HTML tags
*
* @param match - The matched string including < and >
* @returns Whether this is a valid variable reference
*/
private isValidVariableReference(match: string): boolean {
const innerContent = match.slice(1, -1)
if (innerContent.startsWith(' ')) {
return false
}
if (innerContent.match(/^\s*[<>=!]+\s*$/) || innerContent.match(/\s[<>=!]+\s/)) {
return false
}
if (innerContent.match(/^[<>=!]+\s/)) {
return false
}
if (innerContent.includes('.')) {
const dotIndex = innerContent.indexOf('.')
const beforeDot = innerContent.substring(0, dotIndex)
const afterDot = innerContent.substring(dotIndex + 1)
if (afterDot.includes(' ')) {
return false
}
if (beforeDot.match(/[+*/=<>!]/) || afterDot.match(/[+\-*/=<>!]/)) {
return false
}
} else {
if (
innerContent.match(/[+\-*/=<>!]/) ||
innerContent.match(/^\d/) ||
innerContent.match(/\s\d/)
) {
return false
}
}
return true
}
/**
* Determines if a string contains a properly formatted environment variable reference.
* Valid references are either:
* 1. A standalone env var (entire string is just {{ENV_VAR}})
* 2. An explicit env var with clear boundaries (usually within a URL or similar)
*
* @param value - The string to check
* @returns Whether this contains a properly formatted env var reference
*/
private containsProperEnvVarReference(value: string): boolean {
if (!value || typeof value !== 'string') return false
// Case 1: String is just a single environment variable
if (value.trim().match(/^\{\{[^{}]+\}\}$/)) {
return true
}
// Case 2: Check for environment variables in specific contexts
// For example, in URLs, bearer tokens, etc.
const properContextPatterns = [
// Auth header patterns
/Bearer\s+\{\{[^{}]+\}\}/i,
/Authorization:\s+Bearer\s+\{\{[^{}]+\}\}/i,
/Authorization:\s+\{\{[^{}]+\}\}/i,
// API key in URL patterns
/[?&]api[_-]?key=\{\{[^{}]+\}\}/i,
/[?&]key=\{\{[^{}]+\}\}/i,
/[?&]token=\{\{[^{}]+\}\}/i,
// API key in header patterns
/X-API-Key:\s+\{\{[^{}]+\}\}/i,
/api[_-]?key:\s+\{\{[^{}]+\}\}/i,
]
return properContextPatterns.some((pattern) => pattern.test(value))
}
/**
* Resolves environment variables in any value ({{ENV_VAR}}).
* Only processes environment variables in apiKey fields or when explicitly needed.
*
* @param value - Value that may contain environment variable references
* @param isApiKey - Whether this is an API key field (requires special env var handling)
* @returns Value with environment variables resolved
* @throws Error if referenced environment variable is not found
*/
resolveEnvVariables(value: any, isApiKey = false): any {
resolveEnvVariables(value: any): any {
if (typeof value === 'string') {
// Only process environment variables if:
// 1. This is an API key field
// 2. String is a complete environment variable reference ({{ENV_VAR}})
// 3. String contains environment variable references in proper contexts (auth headers, URLs)
const isExplicitEnvVar = value.trim().startsWith('{{') && value.trim().endsWith('}}')
const hasProperEnvVarReferences = this.containsProperEnvVarReference(value)
const envMatches = value.match(/\{\{([^}]+)\}\}/g)
if (envMatches) {
let resolvedValue = value
for (const match of envMatches) {
const envKey = match.slice(2, -2)
const envValue = this.environmentVariables[envKey]
if (isApiKey || isExplicitEnvVar || hasProperEnvVarReferences) {
const envMatches = value.match(/\{\{([^}]+)\}\}/g)
if (envMatches) {
let resolvedValue = value
for (const match of envMatches) {
const envKey = match.slice(2, -2)
const envValue = this.environmentVariables[envKey]
if (envValue === undefined) {
throw new Error(`Environment variable "${envKey}" was not found.`)
}
resolvedValue = resolvedValue.replace(match, envValue)
if (envValue === undefined) {
throw new Error(`Environment variable "${envKey}" was not found.`)
}
return resolvedValue
resolvedValue = resolvedValue.replace(match, envValue)
}
return resolvedValue
}
return value
}
if (Array.isArray(value)) {
return value.map((item) => this.resolveEnvVariables(item, isApiKey))
return value.map((item) => this.resolveEnvVariables(item))
}
if (value && typeof value === 'object') {
return Object.entries(value).reduce(
(acc, [k, v]) => ({
...acc,
[k]: this.resolveEnvVariables(v, k.toLowerCase() === 'apikey'),
[k]: this.resolveEnvVariables(v),
}),
{}
)
@@ -1016,11 +916,8 @@ export class InputResolver {
// Then resolve block references
const resolvedReferences = this.resolveBlockReferences(resolvedVars, context, currentBlock)
// Check if this is an API key field
const isApiKey = this.isApiKeyField(currentBlock, value)
// Then resolve environment variables with the API key flag
return this.resolveEnvVariables(resolvedReferences, isApiKey)
// Then resolve environment variables
return this.resolveEnvVariables(resolvedReferences)
}
// Handle arrays
@@ -1032,7 +929,6 @@ export class InputResolver {
if (typeof value === 'object') {
const result: Record<string, any> = {}
for (const [k, v] of Object.entries(value)) {
const _isApiKey = k.toLowerCase() === 'apikey'
result[k] = this.resolveNestedStructure(v, context, currentBlock)
}
return result
@@ -1042,38 +938,6 @@ export class InputResolver {
return value
}
/**
* Determines if a given field in a block is an API key field.
*
* @param block - Block containing the field
* @param value - Value to check
* @returns Whether this appears to be an API key field
*/
private isApiKeyField(block: SerializedBlock, value: string): boolean {
// Check if the block is an API or agent block (which typically have API keys)
const blockType = block.metadata?.id
if (blockType !== 'api' && blockType !== 'agent') {
return false
}
// Look for the value in the block params
for (const [key, paramValue] of Object.entries(block.config.params)) {
if (paramValue === value) {
// Check if key name suggests it's an API key
const normalizedKey = key.toLowerCase().replace(/[_\-\s]/g, '')
return (
normalizedKey === 'apikey' ||
normalizedKey.includes('apikey') ||
normalizedKey.includes('secretkey') ||
normalizedKey.includes('accesskey') ||
normalizedKey.includes('token')
)
}
}
return false
}
/**
* Formats a value for use in condition blocks.
* Handles strings, null, undefined, and objects appropriately.
@@ -1291,41 +1155,17 @@ export class InputResolver {
return [...new Set(names)] // Remove duplicates
}
/**
* Checks if a block reference could potentially be valid without throwing errors.
* Used to filter out non-block patterns like <test> from block reference resolution.
*
* @param blockRef - The block reference to check
* @param currentBlockId - ID of the current block
* @returns Whether this could be a valid block reference
*/
private isAccessibleBlockReference(blockRef: string, currentBlockId: string): boolean {
// Special cases that are always allowed
const specialRefs = ['start', 'loop', 'parallel']
if (specialRefs.includes(blockRef.toLowerCase())) {
return true
}
// Get all accessible block names for this block
const accessibleNames = this.getAccessibleBlockNames(currentBlockId)
// Check if the reference matches any accessible block name
return accessibleNames.includes(blockRef) || accessibleNames.includes(blockRef.toLowerCase())
}
/**
* Validates if a block reference is accessible from the current block.
* Checks existence and connection-based access rules.
*
* @param blockRef - Name or ID of the referenced block
* @param currentBlockId - ID of the block making the reference
* @param context - Current execution context
* @returns Validation result with success status and resolved block ID or error message
*/
private validateBlockReference(
blockRef: string,
currentBlockId: string,
context: ExecutionContext
currentBlockId: string
): { isValid: boolean; resolvedBlockId?: string; errorMessage?: string } {
// Special case: 'start' is always allowed
if (blockRef.toLowerCase() === 'start') {
@@ -1709,7 +1549,7 @@ export class InputResolver {
} else if (parallel.nodes.includes(currentBlock.id)) {
// Fallback: if we're inside a parallel execution but don't have currentVirtualBlockId
// This shouldn't happen in normal execution but provides backward compatibility
for (const [virtualId, mapping] of context.parallelBlockMapping || new Map()) {
for (const [_, mapping] of context.parallelBlockMapping || new Map()) {
if (mapping.originalBlockId === currentBlock.id && mapping.parallelId === parallelId) {
const iterationKey = `${parallelId}_iteration_${mapping.iterationIndex}`
const iterationItem = context.loopItems.get(iterationKey)
@@ -1899,11 +1739,8 @@ export class InputResolver {
// Then resolve block references
const resolvedReferences = this.resolveBlockReferences(resolvedVars, context, block)
// Check if this is an API key field
const isApiKey = this.isApiKeyField(block, value)
// Then resolve environment variables
const resolvedEnv = this.resolveEnvVariables(resolvedReferences, isApiKey)
const resolvedEnv = this.resolveEnvVariables(resolvedReferences)
// Special handling for different block types
const blockType = block.metadata?.id
@@ -1927,17 +1764,11 @@ export class InputResolver {
* Handles special cases like table-like arrays with cells.
*
* @param value - Object or array to process
* @param key - The parameter key
* @param context - Current execution context
* @param block - Block containing the value
* @returns Processed object/array
*/
private processObjectValue(
value: any,
key: string,
context: ExecutionContext,
block: SerializedBlock
): any {
private processObjectValue(value: any, context: ExecutionContext, block: SerializedBlock): any {
// Special handling for table-like arrays (e.g., from API params/headers)
if (
Array.isArray(value) &&