Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/comp
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand'
import type { GenerationType } from '@/blocks/types'
import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation'
import { useTagSelection } from '@/hooks/use-tag-selection'
import { normalizeBlockName } from '@/stores/workflows/utils'

Expand Down Expand Up @@ -99,14 +100,15 @@ const createHighlightFunction = (
let processedCode = codeToHighlight

// Replace environment variables with placeholders
processedCode = processedCode.replace(/\{\{([^}]+)\}\}/g, (match) => {
processedCode = processedCode.replace(createEnvVarPattern(), (match) => {
const placeholder = `__ENV_VAR_${placeholders.length}__`
placeholders.push({ placeholder, original: match, type: 'env' })
return placeholder
})

// Replace variable references with placeholders
processedCode = processedCode.replace(/<([^>]+)>/g, (match) => {
// Use [^<>]+ to prevent matching across nested brackets (e.g., "<3 <real.ref>" should match separately)
processedCode = processedCode.replace(createReferencePattern(), (match) => {
if (shouldHighlightReference(match)) {
const placeholder = `__VAR_REF_${placeholders.length}__`
placeholders.push({ placeholder, original: match, type: 'var' })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation'
import { useTagSelection } from '@/hooks/use-tag-selection'
import { normalizeBlockName } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
Expand Down Expand Up @@ -869,7 +870,7 @@ export function ConditionInput({
let processedCode = codeToHighlight

// Replace environment variables with placeholders
processedCode = processedCode.replace(/\{\{([^}]+)\}\}/g, (match) => {
processedCode = processedCode.replace(createEnvVarPattern(), (match) => {
const placeholder = `__ENV_VAR_${placeholders.length}__`
placeholders.push({
placeholder,
Expand All @@ -881,20 +882,24 @@ export function ConditionInput({
})

// Replace variable references with placeholders
processedCode = processedCode.replace(/<([^>]+)>/g, (match) => {
const shouldHighlight = shouldHighlightReference(match)
if (shouldHighlight) {
const placeholder = `__VAR_REF_${placeholders.length}__`
placeholders.push({
placeholder,
original: match,
type: 'var',
shouldHighlight: true,
})
return placeholder
// Use [^<>]+ to prevent matching across nested brackets (e.g., "<3 <real.ref>" should match separately)
processedCode = processedCode.replace(
createReferencePattern(),
(match) => {
const shouldHighlight = shouldHighlightReference(match)
if (shouldHighlight) {
const placeholder = `__VAR_REF_${placeholders.length}__`
placeholders.push({
placeholder,
original: match,
type: 'var',
shouldHighlight: true,
})
return placeholder
}
return match
}
return match
})
)

// Apply Prism syntax highlighting
let highlightedCode = highlight(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import type { ReactNode } from 'react'
import { splitReferenceSegment } from '@/lib/workflows/references'
import { REFERENCE } from '@/executor/consts'
import { createCombinedPattern } from '@/executor/utils/reference-validation'
import { normalizeBlockName } from '@/stores/workflows/utils'

export interface HighlightContext {
Expand Down Expand Up @@ -43,7 +45,9 @@ export function formatDisplayText(text: string, context?: HighlightContext): Rea
}

const nodes: ReactNode[] = []
const regex = /<[^>]+>|\{\{[^}]+\}\}/g
// Match variable references without allowing nested brackets to prevent matching across references
// e.g., "<3. text <real.ref>" should match "<3" and "<real.ref>", not the whole string
const regex = createCombinedPattern()
let lastIndex = 0
let key = 0

Expand All @@ -61,7 +65,7 @@ export function formatDisplayText(text: string, context?: HighlightContext): Rea
pushPlainText(text.slice(lastIndex, index))
}

if (matchText.startsWith('{{')) {
if (matchText.startsWith(REFERENCE.ENV_VAR_START)) {
nodes.push(
<span key={key++} className='text-[#34B5FF] dark:text-[#34B5FF]'>
{matchText}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '@/lib/workflows/references'
import { checkTagTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { normalizeBlockName } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
Expand Down Expand Up @@ -133,13 +134,14 @@ export function useSubflowEditor(currentBlock: BlockState | null, currentBlockId

let processedCode = code

processedCode = processedCode.replace(/\{\{([^}]+)\}\}/g, (match) => {
processedCode = processedCode.replace(createEnvVarPattern(), (match) => {
const placeholder = `__ENV_VAR_${placeholders.length}__`
placeholders.push({ placeholder, original: match, type: 'env' })
return placeholder
})

processedCode = processedCode.replace(/<[^>]+>/g, (match) => {
// Use [^<>]+ to prevent matching across nested brackets (e.g., "<3 <real.ref>" should match separately)
processedCode = processedCode.replace(createReferencePattern(), (match) => {
if (shouldHighlightReference(match)) {
const placeholder = `__VAR_REF_${placeholders.length}__`
placeholders.push({ placeholder, original: match, type: 'var' })
Expand Down
7 changes: 3 additions & 4 deletions apps/sim/executor/orchestrators/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { LoopScope } from '@/executor/execution/state'
import type { BlockStateController } from '@/executor/execution/types'
import type { ExecutionContext, NormalizedBlockOutput } from '@/executor/types'
import type { LoopConfigWithNodes } from '@/executor/types/loop'
import { replaceValidReferences } from '@/executor/utils/reference-validation'
import {
buildSentinelEndId,
buildSentinelStartId,
Expand Down Expand Up @@ -271,16 +272,14 @@ export class LoopOrchestrator {
}

try {
const referencePattern = /<([^>]+)>/g
let evaluatedCondition = condition

logger.info('Evaluating loop condition', {
originalCondition: condition,
iteration: scope.iteration,
workflowVariables: ctx.workflowVariables,
})

evaluatedCondition = evaluatedCondition.replace(referencePattern, (match) => {
// Use generic utility for smart variable reference replacement
const evaluatedCondition = replaceValidReferences(condition, (match) => {
const resolved = this.resolver.resolveSingleReference(ctx, '', match, scope)
logger.info('Resolved variable reference in loop condition', {
reference: match,
Expand Down
49 changes: 49 additions & 0 deletions apps/sim/executor/utils/reference-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { isLikelyReferenceSegment } from '@/lib/workflows/references'
import { REFERENCE } from '@/executor/consts'

/**
* Creates a regex pattern for matching variable references.
* Uses [^<>]+ to prevent matching across nested brackets (e.g., "<3 <real.ref>" matches separately).
*/
export function createReferencePattern(): RegExp {
return new RegExp(
`${REFERENCE.START}([^${REFERENCE.START}${REFERENCE.END}]+)${REFERENCE.END}`,
'g'
)
}

/**
* Creates a regex pattern for matching environment variables {{variable}}
*/
export function createEnvVarPattern(): RegExp {
return new RegExp(`\\${REFERENCE.ENV_VAR_START}([^}]+)\\${REFERENCE.ENV_VAR_END}`, 'g')
}

/**
* Combined pattern matching both <reference> and {{env_var}}
*/
export function createCombinedPattern(): RegExp {
return new RegExp(
`${REFERENCE.START}[^${REFERENCE.START}${REFERENCE.END}]+${REFERENCE.END}|` +
`\\${REFERENCE.ENV_VAR_START}[^}]+\\${REFERENCE.ENV_VAR_END}`,
'g'
)
}

/**
* Replaces variable references with smart validation.
* Distinguishes < operator from < bracket using isLikelyReferenceSegment.
*/
export function replaceValidReferences(
template: string,
replacer: (match: string) => string
): string {
const pattern = createReferencePattern()

return template.replace(pattern, (match) => {
if (!isLikelyReferenceSegment(match)) {
return match
}
return replacer(match)
})
}
17 changes: 5 additions & 12 deletions apps/sim/executor/variables/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createLogger } from '@/lib/logs/console/logger'
import { BlockType, REFERENCE } from '@/executor/consts'
import type { ExecutionState, LoopScope } from '@/executor/execution/state'
import type { ExecutionContext } from '@/executor/types'
import { replaceValidReferences } from '@/executor/utils/reference-validation'
import { BlockResolver } from '@/executor/variables/resolvers/block'
import { EnvResolver } from '@/executor/variables/resolvers/env'
import { LoopResolver } from '@/executor/variables/resolvers/loop'
Expand Down Expand Up @@ -147,21 +148,17 @@ export class VariableResolver {
loopScope?: LoopScope,
block?: SerializedBlock
): string {
let result = template
const resolutionContext: ResolutionContext = {
executionContext: ctx,
executionState: this.state,
currentNodeId,
loopScope,
}
const referenceRegex = new RegExp(
`${REFERENCE.START}([^${REFERENCE.END}]+)${REFERENCE.END}`,
'g'
)

let replacementError: Error | null = null

result = result.replace(referenceRegex, (match) => {
// Use generic utility for smart variable reference replacement
let result = replaceValidReferences(template, (match) => {
if (replacementError) return match

try {
Expand Down Expand Up @@ -202,21 +199,17 @@ export class VariableResolver {
template: string,
loopScope?: LoopScope
): string {
let result = template
const resolutionContext: ResolutionContext = {
executionContext: ctx,
executionState: this.state,
currentNodeId,
loopScope,
}
const referenceRegex = new RegExp(
`${REFERENCE.START}([^${REFERENCE.END}]+)${REFERENCE.END}`,
'g'
)

let replacementError: Error | null = null

result = result.replace(referenceRegex, (match) => {
// Use generic utility for smart variable reference replacement
let result = replaceValidReferences(template, (match) => {
if (replacementError) return match

try {
Expand Down