diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/debug/debug.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/debug/debug.tsx index 29cc46f667..4d16b775a6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/debug/debug.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/debug/debug.tsx @@ -12,6 +12,11 @@ import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId] import { getBlock } from '@/blocks' import { mergeSubblockState } from '@/stores/workflows/utils' import { useWorkflowStore } from '@/stores/workflows/workflow/store' +import { BlockPathCalculator } from '@/lib/block-path-calculator' +import { useSubBlockStore } from '@/stores/workflows/subblock/store' +import { extractFieldsFromSchema, parseResponseFormatSafely } from '@/lib/response-format' +import { getTrigger, getTriggersByProvider } from '@/triggers' +import { getTool } from '@/tools/utils' export function DebugPanel() { const { isDebugging, pendingBlocks, debugContext, activeBlockIds, setActiveBlocks, setPanelFocusedBlockId, panelFocusedBlockId } = useExecutionStore() @@ -23,6 +28,12 @@ export function DebugPanel() { const hasStartedRef = useRef(false) const lastFocusedIdRef = useRef(null) + // Helper to consistently resolve a human-readable block name + const getDisplayName = (block: any | null | undefined): string => { + if (!block) return '' + return block.name || block.metadata?.name || block.type || block.id || '' + } + // Always use current workflow blocks as the source of truth // This ensures consistency whether debugContext exists or not const blocksList = useMemo(() => { @@ -182,6 +193,143 @@ export function DebugPanel() { return result }, [focusedBlock, focusedBlockId, debugContext?.blockStates]) + // Compute accessible output variables for the focused block with tag-style references + const outputVariableEntries = useMemo(() => { + if (!focusedBlockId) return [] as Array<{ ref: string; value: any }> + + const normalizeBlockName = (name: string) => (name || '').replace(/\s+/g, '').toLowerCase() + const getSubBlockValue = (blockId: string, property: string): any => { + return useSubBlockStore.getState().getValue(blockId, property) + } + const generateOutputPaths = (outputs: Record, prefix = ''): string[] => { + const paths: string[] = [] + for (const [key, value] of Object.entries(outputs || {})) { + const current = prefix ? `${prefix}.${key}` : key + if (typeof value === 'string') { + paths.push(current) + } else if (value && typeof value === 'object') { + if ('type' in value && typeof (value as any).type === 'string') { + paths.push(current) + if ((value as any).type === 'object' && (value as any).properties) { + paths.push(...generateOutputPaths((value as any).properties, current)) + } else if ((value as any).type === 'array' && (value as any).items?.properties) { + paths.push(...generateOutputPaths((value as any).items.properties, current)) + } + } else { + paths.push(...generateOutputPaths(value as Record, current)) + } + } else { + paths.push(current) + } + } + return paths + } + + const getAccessiblePathsForBlock = (blockId: string): string[] => { + const blk = blockById.get(blockId) + if (!blk) return [] + const cfg = getBlock(blk.type) + if (!cfg) return [] + + // Response format overrides + const responseFormatValue = getSubBlockValue(blockId, 'responseFormat') + const responseFormat = parseResponseFormatSafely(responseFormatValue, blockId) + if (responseFormat) { + const fields = extractFieldsFromSchema(responseFormat) + if (fields.length > 0) return fields.map((f: any) => f.name) + } + + if (blk.type === 'evaluator') { + const metricsValue = getSubBlockValue(blockId, 'metrics') + if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) { + const valid = metricsValue.filter((m: { name?: string }) => m?.name) + return valid.map((m: { name: string }) => m.name.toLowerCase()) + } + return generateOutputPaths(cfg.outputs || {}) + } + + if (blk.type === 'starter') { + const startWorkflowValue = getSubBlockValue(blockId, 'startWorkflow') + if (startWorkflowValue === 'chat') { + return ['input', 'conversationId', 'files'] + } + const inputFormatValue = getSubBlockValue(blockId, 'inputFormat') + if (inputFormatValue && Array.isArray(inputFormatValue)) { + return inputFormatValue + .filter((f: { name?: string }) => f.name && f.name.trim() !== '') + .map((f: { name: string }) => f.name) + } + return [] + } + + if (blk.triggerMode && cfg.triggers?.enabled) { + const triggerId = cfg?.triggers?.available?.[0] + const firstTrigger = triggerId ? getTrigger(triggerId) : getTriggersByProvider(blk.type)[0] + if (firstTrigger?.outputs) { + return generateOutputPaths(firstTrigger.outputs) + } + } + + const operationValue = getSubBlockValue(blockId, 'operation') + if (operationValue && cfg?.tools?.config?.tool) { + try { + const toolId = cfg.tools.config.tool({ operation: operationValue }) + const toolConfig = toolId ? getTool(toolId) : null + if (toolConfig?.outputs) return generateOutputPaths(toolConfig.outputs) + } catch {} + } + + return generateOutputPaths(cfg.outputs || {}) + } + + const edges = currentWorkflow.edges || [] + const accessibleIds = new Set(BlockPathCalculator.findAllPathNodes(edges, focusedBlockId)) + + // Always allow referencing the starter block + if (starterId && starterId !== focusedBlockId) accessibleIds.add(starterId) + + const entries: Array<{ ref: string; value: any }> = [] + + for (const id of accessibleIds) { + const blk = blockById.get(id) + if (!blk) continue + + const allowedPathsSet = new Set(getAccessiblePathsForBlock(id)) + if (allowedPathsSet.size === 0) continue + + const displayName = getDisplayName(blk) + const normalizedName = normalizeBlockName(displayName) + + const executedOutput = debugContext?.blockStates.get(id)?.output || {} + + // Flatten executed outputs and include only those matching allowed paths + const flatten = (obj: any, prefix = ''): Array<{ path: string; value: any }> => { + if (obj == null || typeof obj !== 'object') return [] + const items: Array<{ path: string; value: any }> = [] + for (const [k, v] of Object.entries(obj)) { + const current = prefix ? `${prefix}.${k}` : k + if (v && typeof v === 'object' && !Array.isArray(v)) { + // include the object level only if explicitly allowed + if (allowedPathsSet.has(current)) items.push({ path: current, value: v }) + items.push(...flatten(v, current)) + } else { + if (allowedPathsSet.has(current)) items.push({ path: current, value: v }) + } + } + return items + } + + const executedPairs = flatten(executedOutput) + for (const { path, value } of executedPairs) { + entries.push({ ref: `<${normalizedName}.${path}>`, value }) + } + } + + // Sort for stable UI (by ref) + entries.sort((a, b) => a.ref.localeCompare(b.ref)) + return entries + }, [focusedBlockId, currentWorkflow.edges, starterId, blockById, debugContext?.blockStates]) + // Reset hasStartedRef when debug mode is deactivated useEffect(() => { if (!isDebugging) { @@ -230,11 +378,11 @@ export function DebugPanel() { {/* Header with block title and status */}
- {focusedBlock?.metadata?.name || focusedBlock?.id || '—'} + {getDisplayName(focusedBlock) || '—'}
- {focusedBlock?.metadata?.id} + {focusedBlock?.type} {isFocusedPending ? 'Current' : isFocusedExecuted ? 'Executed' : 'Not in execution path'} @@ -293,24 +441,33 @@ export function DebugPanel() {
- {/* Expandable Variables */} -
- Variables -
-
-
Environment
- {envVars && Object.keys(envVars).length > 0 ? ( - -
-{JSON.stringify(envVars, null, 2)}
-                
+ {/* Variables: three collapsible subsections */} +
+
+ Output variables +
+ {outputVariableEntries.length > 0 ? ( + +
+ {outputVariableEntries.map(({ ref, value }) => ( +
+
{ref}
+
+{JSON.stringify(value, null, 2)}
+                      
+
+ ))} +
) : (
None
)}
-
-
Workflow
+
+ +
+ Workflow variables +
{workflowVars && Object.keys(workflowVars).length > 0 ? (
@@ -321,8 +478,23 @@ export function DebugPanel() {
               
None
)}
-
-
+ + +
+ Environment variables +
+ {envVars && Object.keys(envVars).length > 0 ? ( + +
+{JSON.stringify(envVars, null, 2)}
+                
+
+ ) : ( +
None
+ )} +
+
+
) } \ No newline at end of file