diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/code/code.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/code/code.tsx index 8fec01358..786de33f2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/code/code.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/code/code.tsx @@ -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' @@ -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 " should match separately) + processedCode = processedCode.replace(createReferencePattern(), (match) => { if (shouldHighlightReference(match)) { const placeholder = `__VAR_REF_${placeholders.length}__` placeholders.push({ placeholder, original: match, type: 'var' }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/condition-input/condition-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/condition-input/condition-input.tsx index d3972f1eb..96014130c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/condition-input/condition-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/condition-input/condition-input.tsx @@ -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' @@ -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, @@ -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 " 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( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/formatted-text.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/formatted-text.tsx index ff97cf471..6ce3443f3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/formatted-text.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/formatted-text.tsx @@ -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 { @@ -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 " should match "<3" and "", not the whole string + const regex = createCombinedPattern() let lastIndex = 0 let key = 0 @@ -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( {matchText} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/hooks/use-subflow-editor.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/hooks/use-subflow-editor.ts index c238c2801..96516bcc7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/hooks/use-subflow-editor.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/hooks/use-subflow-editor.ts @@ -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' @@ -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 " should match separately) + processedCode = processedCode.replace(createReferencePattern(), (match) => { if (shouldHighlightReference(match)) { const placeholder = `__VAR_REF_${placeholders.length}__` placeholders.push({ placeholder, original: match, type: 'var' }) diff --git a/apps/sim/executor/orchestrators/loop.ts b/apps/sim/executor/orchestrators/loop.ts index dcc2f82f0..93dde42b0 100644 --- a/apps/sim/executor/orchestrators/loop.ts +++ b/apps/sim/executor/orchestrators/loop.ts @@ -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, @@ -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, diff --git a/apps/sim/executor/utils/reference-validation.ts b/apps/sim/executor/utils/reference-validation.ts new file mode 100644 index 000000000..906af3287 --- /dev/null +++ b/apps/sim/executor/utils/reference-validation.ts @@ -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 " 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 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) + }) +} diff --git a/apps/sim/executor/variables/resolver.ts b/apps/sim/executor/variables/resolver.ts index c8641e0bb..d5aa1dfba 100644 --- a/apps/sim/executor/variables/resolver.ts +++ b/apps/sim/executor/variables/resolver.ts @@ -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' @@ -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 { @@ -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 {