From d236cc8ad04c4e91bc38984514444d62f62bc7f1 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 9 Feb 2026 09:18:45 -0800 Subject: [PATCH 01/17] refactor(tool-input): eliminate SyncWrappers, add canonical toggle and dependsOn gating Replace 17+ individual SyncWrapper components with a single centralized ToolSubBlockRenderer that bridges the subblock store with StoredTool.params via synthetic store keys. This reduces ~1000 lines of duplicated wrapper code and ensures tool-input renders subblock components identically to the standalone SubBlock path. - Add ToolSubBlockRenderer with bidirectional store sync - Add basic/advanced mode toggle (ArrowLeftRight) using collaborative functions - Add dependsOn gating via useDependsOnGate (fields disable instead of hiding) - Add paramVisibility field to SubBlockConfig for tool-input visibility control - Pass canonicalModeOverrides through getSubBlocksForToolInput - Show (optional) label for non-user-only fields (LLM can inject at runtime) Co-Authored-By: Claude Opus 4.6 --- .../components/tool-sub-block-renderer.tsx | 410 +++++ .../components/tool-input/tool-input.tsx | 1377 +++++------------ apps/sim/blocks/types.ts | 2 + apps/sim/tools/params-resolver.ts | 4 + apps/sim/tools/params.ts | 210 ++- 5 files changed, 994 insertions(+), 1009 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-sub-block-renderer.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-sub-block-renderer.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-sub-block-renderer.tsx new file mode 100644 index 0000000000..bb8c26f9c5 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-sub-block-renderer.tsx @@ -0,0 +1,410 @@ +'use client' + +import type React from 'react' +import { useCallback, useEffect, useRef } from 'react' +import { Combobox, Switch } from '@/components/emcn' +import { + CheckboxList, + Code, + DocumentSelector, + DocumentTagEntry, + FileSelectorInput, + FileUpload, + FolderSelectorInput, + KnowledgeBaseSelector, + KnowledgeTagFilters, + LongInput, + ProjectSelectorInput, + SheetSelectorInput, + ShortInput, + SlackSelectorInput, + SliderInput, + Table, + TimeInput, + WorkflowSelectorInput, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components' +import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' +import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' +import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block' +import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types' +import { isPasswordParameter } from '@/tools/params' + +interface ToolSubBlockRendererProps { + blockId: string + subBlockId: string + toolIndex: number + subBlock: BlockSubBlockConfig + effectiveParamId: string + toolParams: Record | undefined + onParamChange: (toolIndex: number, paramId: string, value: string) => void + disabled: boolean + previewContextValues?: Record + wandControlRef?: React.MutableRefObject +} + +/** + * Renders a subblock component inside tool-input by bridging the subblock store + * with StoredTool.params via a synthetic store key. + * + * Replaces the 17+ individual SyncWrapper components that previously existed. + * Components read/write to the store at a synthetic ID, and two effects + * handle bidirectional sync with tool.params. + */ +export function ToolSubBlockRenderer({ + blockId, + subBlockId, + toolIndex, + subBlock, + effectiveParamId, + toolParams, + onParamChange, + disabled, + previewContextValues, + wandControlRef, +}: ToolSubBlockRendererProps) { + const syntheticId = `${subBlockId}-tool-${toolIndex}-${effectiveParamId}` + const [storeValue, setStoreValue] = useSubBlockValue(blockId, syntheticId) + + // Gate the component using the same dependsOn logic as SubBlock + const { finalDisabled } = useDependsOnGate(blockId, subBlock, { + disabled, + previewContextValues, + }) + + const toolParamValue = toolParams?.[effectiveParamId] ?? '' + + /** Tracks the last value we wrote to the store from tool.params to avoid echo loops */ + const lastInitRef = useRef(toolParamValue) + /** Tracks the last value we synced back to tool.params from the store */ + const lastSyncRef = useRef(toolParamValue) + + // Init effect: push tool.params value into the store when it changes externally + useEffect(() => { + if (toolParamValue !== lastInitRef.current) { + lastInitRef.current = toolParamValue + lastSyncRef.current = toolParamValue + setStoreValue(toolParamValue) + } + }, [toolParamValue, setStoreValue]) + + // Sync effect: when the store changes (user interaction), push back to tool.params + useEffect(() => { + if (storeValue == null) return + const stringValue = typeof storeValue === 'string' ? storeValue : JSON.stringify(storeValue) + if (stringValue !== lastSyncRef.current) { + lastSyncRef.current = stringValue + lastInitRef.current = stringValue + onParamChange(toolIndex, effectiveParamId, stringValue) + } + }, [storeValue, toolIndex, effectiveParamId, onParamChange]) + + // Initialize the store on first mount + const hasInitializedRef = useRef(false) + useEffect(() => { + if (!hasInitializedRef.current && toolParamValue) { + hasInitializedRef.current = true + setStoreValue(toolParamValue) + } + }, []) // eslint-disable-line react-hooks/exhaustive-deps + + const configWithSyntheticId = { ...subBlock, id: syntheticId } + + return renderSubBlockComponent({ + blockId, + syntheticId, + config: configWithSyntheticId, + subBlock, + disabled: finalDisabled, + previewContextValues, + wandControlRef, + toolParamValue, + onParamChange: useCallback( + (value: string) => onParamChange(toolIndex, effectiveParamId, value), + [toolIndex, effectiveParamId, onParamChange] + ), + }) +} + +interface RenderContext { + blockId: string + syntheticId: string + config: BlockSubBlockConfig + subBlock: BlockSubBlockConfig + disabled: boolean + previewContextValues?: Record + wandControlRef?: React.MutableRefObject + toolParamValue: string + onParamChange: (value: string) => void +} + +/** + * Renders the appropriate component for a subblock type. + * Mirrors the switch cases in SubBlock's renderInput(), using + * the same component props pattern. + */ +function renderSubBlockComponent(ctx: RenderContext): React.ReactNode { + const { + blockId, + syntheticId, + config, + subBlock, + disabled, + previewContextValues, + wandControlRef, + toolParamValue, + onParamChange, + } = ctx + + switch (subBlock.type) { + case 'short-input': + return ( + + ) + + case 'long-input': + return ( + + ) + + case 'dropdown': + return ( + option.id !== '') + .map((option) => ({ + label: option.label, + value: option.id, + })) || [] + } + value={toolParamValue} + onChange={onParamChange} + placeholder={subBlock.placeholder || 'Select option'} + disabled={disabled} + /> + ) + + case 'switch': + return ( + onParamChange(checked ? 'true' : 'false')} + /> + ) + + case 'code': + return ( + + ) + + case 'channel-selector': + case 'user-selector': + return ( + + ) + + case 'project-selector': + return ( + + ) + + case 'file-selector': + return ( + + ) + + case 'sheet-selector': + return ( + + ) + + case 'folder-selector': + return ( + + ) + + case 'knowledge-base-selector': + return + + case 'document-selector': + return ( + + ) + + case 'document-tag-entry': + return ( + + ) + + case 'knowledge-tag-filters': + return ( + + ) + + case 'table': + return ( + + ) + + case 'slider': + return ( + + ) + + case 'checkbox-list': + return ( + + ) + + case 'time-input': + return ( + + ) + + case 'file-upload': + return ( + + ) + + case 'combobox': + return ( + ({ + label: opt.label, + value: opt.id, + }) + )} + value={toolParamValue} + onChange={onParamChange} + placeholder={subBlock.placeholder || 'Select option'} + disabled={disabled} + /> + ) + + case 'workflow-selector': + return + + case 'oauth-input': + // OAuth inputs are handled separately by ToolCredentialSelector in the parent + return null + + default: + return ( + + ) + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index ff08547ec9..49ba83688b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -1,13 +1,16 @@ import type React from 'react' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import { Loader2, WrenchIcon, XIcon } from 'lucide-react' +import { ArrowLeftRight, ArrowUp, Loader2, WrenchIcon, XIcon } from 'lucide-react' import { useParams } from 'next/navigation' import { Badge, + Button, Combobox, type ComboboxOption, type ComboboxOptionGroup, + Input, + Label, Popover, PopoverContent, PopoverItem, @@ -32,31 +35,19 @@ import { import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { - CheckboxList, - Code, - FileSelectorInput, - FileUpload, - FolderSelectorInput, LongInput, - ProjectSelectorInput, - SheetSelectorInput, ShortInput, - SlackSelectorInput, - SliderInput, - Table, - TimeInput, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components' -import { DocumentSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/document-selector/document-selector' -import { DocumentTagEntry } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/document-tag-entry/document-tag-entry' -import { KnowledgeBaseSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-base-selector/knowledge-base-selector' -import { KnowledgeTagFilters } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters' import { type CustomTool, CustomToolModal, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal' import { ToolCredentialSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector' +import { ToolSubBlockRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-sub-block-renderer' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' +import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block' import { getAllBlocks } from '@/blocks' +import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types' import { useMcpTools } from '@/hooks/mcp/use-mcp-tools' import { type CustomTool as CustomToolDefinition, @@ -74,26 +65,217 @@ import { useWorkflowState, useWorkflows, } from '@/hooks/queries/workflows' +import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { usePermissionConfig } from '@/hooks/use-permission-config' import { getProviderFromModel, supportsToolUsageControl } from '@/providers/utils' import { useSettingsModalStore } from '@/stores/modals/settings/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { formatParameterLabel, + getSubBlocksForToolInput, getToolParametersConfig, isPasswordParameter, + type SubBlocksForToolInput, type ToolParameterConfig, } from '@/tools/params' import { buildCanonicalIndex, buildPreviewContextValues, type CanonicalIndex, + type CanonicalModeOverrides, evaluateSubBlockCondition, + isCanonicalPair, + resolveCanonicalMode, type SubBlockCondition, } from '@/tools/params-resolver' const logger = createLogger('ToolInput') +/** + * Props for a generic parameter with label component + */ +interface ParameterWithLabelProps { + paramId: string + title: string + isRequired: boolean + visibility: string + wandConfig?: { + enabled: boolean + prompt?: string + placeholder?: string + } + canonicalToggle?: { + mode: 'basic' | 'advanced' + disabled?: boolean + onToggle?: () => void + } + disabled: boolean + isPreview: boolean + children: (wandControlRef: React.MutableRefObject) => React.ReactNode +} + +/** + * Generic wrapper component for parameters that manages wand state and renders label + input + */ +const ParameterWithLabel: React.FC = ({ + paramId, + title, + isRequired, + visibility, + wandConfig, + canonicalToggle, + disabled, + isPreview, + children, +}) => { + const [isSearchActive, setIsSearchActive] = useState(false) + const [searchQuery, setSearchQuery] = useState('') + const searchInputRef = useRef(null) + const wandControlRef = useRef(null) + + const isWandEnabled = wandConfig?.enabled ?? false + const showWand = isWandEnabled && !isPreview && !disabled + + const handleSearchClick = (): void => { + setIsSearchActive(true) + setTimeout(() => { + searchInputRef.current?.focus() + }, 0) + } + + const handleSearchBlur = (): void => { + if (!searchQuery.trim() && !wandControlRef.current?.isWandStreaming) { + setIsSearchActive(false) + } + } + + const handleSearchChange = (value: string): void => { + setSearchQuery(value) + } + + const handleSearchSubmit = (): void => { + if (searchQuery.trim() && wandControlRef.current) { + wandControlRef.current.onWandTrigger(searchQuery) + setSearchQuery('') + setIsSearchActive(false) + } + } + + const handleSearchCancel = (): void => { + setSearchQuery('') + setIsSearchActive(false) + } + + const isStreaming = wandControlRef.current?.isWandStreaming ?? false + + return ( +
+
+ +
+ {showWand && ( + <> + {!isSearchActive ? ( + + ) : ( +
+ ) => + handleSearchChange(e.target.value) + } + onBlur={(e: React.FocusEvent) => { + const relatedTarget = e.relatedTarget as HTMLElement | null + if (relatedTarget?.closest('button')) return + handleSearchBlur() + }} + onKeyDown={(e: React.KeyboardEvent) => { + if (e.key === 'Enter' && searchQuery.trim() && !isStreaming) { + handleSearchSubmit() + } else if (e.key === 'Escape') { + handleSearchCancel() + } + }} + disabled={isStreaming} + className={cn( + 'h-5 min-w-[80px] flex-1 text-[11px]', + isStreaming && 'text-muted-foreground' + )} + placeholder='Generate with AI...' + /> + +
+ )} + + )} + {canonicalToggle && !isPreview && ( + + + + + +

+ {canonicalToggle.mode === 'advanced' + ? 'Switch to selector' + : 'Switch to manual ID'} +

+
+
+ )} +
+
+
{children(wandControlRef)}
+
+ ) +} + /** * Props for the ToolInput component */ @@ -166,590 +348,30 @@ function resolveCustomToolFromReference( return { schema: customTool.schema, code: customTool.code, - title: customTool.title, - } - } - // If not found by ID, fall through to try other methods - logger.warn(`Custom tool not found by ID: ${storedTool.customToolId}`) - } - - // Legacy format: inline schema and code - if (storedTool.schema && storedTool.code !== undefined) { - return { - schema: storedTool.schema, - code: storedTool.code, - title: storedTool.title || '', - } - } - - return null -} - -/** - * Generic sync wrapper that synchronizes store values with local component state. - * - * @remarks - * Used to sync tool parameter values between the workflow store and local controlled inputs. - * Listens for changes in the store and propagates them to the local component via onChange. - * - * @typeParam T - The type of the store value being synchronized - * - * @param blockId - The block identifier for store lookup - * @param paramId - The parameter identifier within the block - * @param value - Current local value - * @param onChange - Callback to update the local value - * @param children - Child components to render - * @param transformer - Optional function to transform store value before comparison - * @returns The children wrapped with synchronization logic - */ -function GenericSyncWrapper({ - blockId, - paramId, - value, - onChange, - children, - transformer, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - children: React.ReactNode - transformer?: (storeValue: T) => string -}) { - const [storeValue] = useSubBlockValue(blockId, paramId) - - useEffect(() => { - if (storeValue != null) { - const transformedValue = transformer ? transformer(storeValue) : String(storeValue) - if (transformedValue !== value) { - onChange(transformedValue) - } - } - }, [storeValue, value, onChange, transformer]) - - return <>{children} -} - -function FileSelectorSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, - previewContextValues, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean - previewContextValues?: Record -}) { - return ( - - - - ) -} - -function SheetSelectorSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, - previewContextValues, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean - previewContextValues?: Record -}) { - return ( - - - - ) -} - -function FolderSelectorSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, - previewContextValues, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean - previewContextValues?: Record -}) { - return ( - - - - ) -} - -function KnowledgeBaseSelectorSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean -}) { - return ( - - - - ) -} - -function DocumentSelectorSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, - previewContextValues, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean - previewContextValues?: Record -}) { - return ( - - - - ) -} - -function DocumentTagEntrySyncWrapper({ - blockId, - paramId, - value, - onChange, - disabled, - previewContextValues, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - disabled: boolean - previewContextValues?: Record -}) { - return ( - - - - ) -} - -function KnowledgeTagFiltersSyncWrapper({ - blockId, - paramId, - value, - onChange, - disabled, - previewContextValues, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - disabled: boolean - previewContextValues?: Record -}) { - return ( - - - - ) -} - -function TableSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean -}) { - return ( - JSON.stringify(storeValue)} - > -
- - ) -} - -function TimeInputSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean -}) { - return ( - - - - ) -} - -function SliderInputSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean -}) { - return ( - String(storeValue)} - > - - - ) -} - -function CheckboxListSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean -}) { - return ( - JSON.stringify(storeValue)} - > - - - ) -} - -function ComboboxSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean -}) { - const options = (uiComponent.options || []).map((opt: any) => - typeof opt === 'string' ? { label: opt, value: opt } : { label: opt.label, value: opt.id } - ) - - return ( - - - - ) -} - -function FileUploadSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean -}) { - return ( - JSON.stringify(storeValue)} - > - - - ) -} - -function SlackSelectorSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, - previewContextValues, - selectorType, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean - previewContextValues?: Record - selectorType: 'channel-selector' | 'user-selector' -}) { - return ( - - - - ) -} - -function WorkflowSelectorSyncWrapper({ - blockId, - paramId, - value, - onChange, - uiComponent, - disabled, - workspaceId, - currentWorkflowId, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - uiComponent: any - disabled: boolean - workspaceId: string - currentWorkflowId?: string -}) { - const { data: workflows = [], isLoading } = useWorkflows(workspaceId, { syncRegistry: false }) - - const availableWorkflows = workflows.filter( - (w) => !currentWorkflowId || w.id !== currentWorkflowId - ) + title: customTool.title, + } + } + // If not found by ID, fall through to try other methods + logger.warn(`Custom tool not found by ID: ${storedTool.customToolId}`) + } - const options = availableWorkflows.map((workflow) => ({ - label: workflow.name, - value: workflow.id, - })) + // Legacy format: inline schema and code + if (storedTool.schema && storedTool.code !== undefined) { + return { + schema: storedTool.schema, + code: storedTool.code, + title: storedTool.title || '', + } + } - return ( - - - - ) + return null } -function WorkflowInputMapperSyncWrapper({ +/** + * Renders the input for workflow_executor's inputMapping parameter. + * This is a special case that doesn't map to any SubBlockConfig, so it's kept here. + */ +function WorkflowInputMapperInput({ blockId, paramId, value, @@ -779,7 +401,7 @@ function WorkflowInputMapperSyncWrapper({ }, [value]) const handleFieldChange = useCallback( - (fieldName: string, fieldValue: any) => { + (fieldName: string, fieldValue: string) => { const newValue = { ...parsedValue, [fieldName]: fieldValue } onChange(JSON.stringify(newValue)) }, @@ -812,7 +434,7 @@ function WorkflowInputMapperSyncWrapper({ return (
- {inputFields.map((field: any) => ( + {inputFields.map((field: { name: string; type: string }) => ( void - disabled: boolean - uiComponent: any - currentToolParams?: Record -}) { - const language = (currentToolParams?.language as 'javascript' | 'python') || 'javascript' - - return ( - - - - ) -} - /** * Badge component showing deployment status for workflow tools */ @@ -1014,6 +598,14 @@ export const ToolInput = memo(function ToolInput({ const [dragOverIndex, setDragOverIndex] = useState(null) const [usageControlPopoverIndex, setUsageControlPopoverIndex] = useState(null) + const canonicalModeOverrides = useWorkflowStore( + useCallback( + (state) => state.blocks[blockId]?.data?.canonicalModes as CanonicalModeOverrides | undefined, + [blockId] + ) + ) + const { collaborativeSetBlockCanonicalMode } = useCollaborativeWorkflow() + const value = isPreview ? previewValue : storeValue const selectedTools: StoredTool[] = @@ -1358,26 +950,6 @@ export const ToolInput = memo(function ToolInput({ return block.tools.access[0] } - /** - * Initializes tool parameters with empty values. - * - * @remarks - * Returns an empty object as parameters are populated dynamically - * based on user input and default values from the tool configuration. - * - * @param toolId - The tool identifier - * @param params - Array of parameter configurations - * @param instanceId - Optional instance identifier for unique param keys - * @returns Empty parameter object to be populated by the user - */ - const initializeToolParams = ( - toolId: string, - params: ToolParameterConfig[], - instanceId?: string - ): Record => { - return {} - } - const handleSelectTool = useCallback( (toolBlock: (typeof toolBlocks)[0]) => { if (isPreview || disabled) return @@ -1394,7 +966,7 @@ export const ToolInput = memo(function ToolInput({ const toolParams = getToolParametersConfig(toolId, toolBlock.type) if (!toolParams) return - const initialParams = initializeToolParams(toolId, toolParams.userInputParameters, blockId) + const initialParams: Record = {} toolParams.userInputParameters.forEach((param) => { if (param.uiComponent?.value && !initialParams[param.id]) { @@ -1427,7 +999,6 @@ export const ToolInput = memo(function ToolInput({ getOperationOptions, getToolIdForOperation, isToolAlreadySelected, - initializeToolParams, blockId, selectedTools, setStoreValue, @@ -1597,7 +1168,7 @@ export const ToolInput = memo(function ToolInput({ return } - const initialParams = initializeToolParams(newToolId, toolParams.userInputParameters, blockId) + const initialParams: Record = {} const oldToolParams = tool.toolId ? getToolParametersConfig(tool.toolId, tool.type) : null const oldParamIds = new Set(oldToolParams?.userInputParameters.map((p) => p.id) || []) @@ -1632,15 +1203,7 @@ export const ToolInput = memo(function ToolInput({ ) ) }, - [ - isPreview, - disabled, - selectedTools, - getToolIdForOperation, - initializeToolParams, - blockId, - setStoreValue, - ] + [isPreview, disabled, selectedTools, getToolIdForOperation, blockId, setStoreValue] ) const handleUsageControlChange = useCallback( @@ -1966,26 +1529,18 @@ export const ToolInput = memo(function ToolInput({ } /** - * Renders the appropriate UI component for a tool parameter. - * - * @remarks - * Supports multiple input types including dropdown, switch, long-input, - * short-input, file-selector, table, slider, and more. Falls back to - * ShortInput for unknown types. + * Renders a parameter input for custom tools, MCP tools, and legacy registry + * tools that don't have SubBlockConfig definitions. * - * @param param - The parameter configuration defining the input type - * @param value - The current parameter value - * @param onChange - Callback to handle value changes - * @param toolIndex - Index of the tool in the selected tools array - * @param currentToolParams - Current values of all tool parameters for dependencies - * @returns JSX element for the parameter input component + * Registry tools with subBlocks use ToolSubBlockRenderer instead. */ const renderParameterInput = ( param: ToolParameterConfig, value: string, onChange: (value: string) => void, toolIndex?: number, - currentToolParams?: Record + currentToolParams?: Record, + wandControlRef?: React.MutableRefObject ) => { const uniqueSubBlockId = toolIndex !== undefined @@ -2007,6 +1562,8 @@ export const ToolInput = memo(function ToolInput({ }} value={value} onChange={onChange} + wandControlRef={wandControlRef} + hideInternalWand={true} /> ) } @@ -2016,11 +1573,11 @@ export const ToolInput = memo(function ToolInput({ return ( option.id !== '') - .map((option: any) => ({ + (uiComponent.options as { id?: string; label: string; value?: string }[] | undefined) + ?.filter((option) => (option.id ?? option.value) !== '') + .map((option) => ({ label: option.label, - value: option.id, + value: option.id ?? option.value ?? '', })) || [] } value={value} @@ -2048,9 +1605,12 @@ export const ToolInput = memo(function ToolInput({ id: uniqueSubBlockId, type: 'long-input', title: param.id, + wandConfig: uiComponent.wandConfig, }} value={value} onChange={onChange} + wandControlRef={wandControlRef} + hideInternalWand={true} /> ) @@ -2065,58 +1625,13 @@ export const ToolInput = memo(function ToolInput({ id: uniqueSubBlockId, type: 'short-input', title: param.id, + wandConfig: uiComponent.wandConfig, }} value={value} onChange={onChange} disabled={disabled} - /> - ) - - case 'channel-selector': - return ( - - ) - - case 'user-selector': - return ( - - ) - - case 'project-selector': - return ( - ) @@ -2132,135 +1647,10 @@ export const ToolInput = memo(function ToolInput({ /> ) - case 'file-selector': - return ( - - ) - - case 'sheet-selector': - return ( - - ) - - case 'folder-selector': - return ( - - ) - - case 'table': - return ( - - ) - - case 'combobox': - return ( - - ) - - case 'slider': - return ( - - ) - - case 'checkbox-list': - return ( - - ) - - case 'time-input': - return ( - - ) - - case 'file-upload': - return ( - - ) - - case 'workflow-selector': - return ( - - ) - case 'workflow-input-mapper': { const selectedWorkflowId = currentToolParams?.workflowId || '' return ( - - ) - - case 'knowledge-base-selector': - return ( - - ) - - case 'document-selector': - return ( - - ) - - case 'document-tag-entry': - return ( - - ) - - case 'knowledge-tag-filters': - return ( - - ) - default: return ( ) } @@ -2394,6 +1724,20 @@ export const ToolInput = memo(function ToolInput({ }) : null + // Get subblocks for tool-input (primary source for registry tools) + const subBlocksResult: SubBlocksForToolInput | null = + !isCustomTool && !isMcpTool && currentToolId + ? getSubBlocksForToolInput( + currentToolId, + tool.type, + { + operation: tool.operation, + ...tool.params, + }, + canonicalModeOverrides + ) + : null + // Build canonical index for proper dependency resolution const toolCanonicalIndex: CanonicalIndex | null = toolBlock?.subBlocks ? buildCanonicalIndex(toolBlock.subBlocks) @@ -2452,11 +1796,16 @@ export const ToolInput = memo(function ToolInput({ : [] // Get all parameters to display - const displayParams = isCustomTool + // For registry tools with subBlocks, use the subblock-first approach + const useSubBlocks = !isCustomTool && !isMcpTool && subBlocksResult?.subBlocks?.length + const displayParams: ToolParameterConfig[] = isCustomTool ? customToolParams : isMcpTool ? mcpToolParams : toolParams?.userInputParameters || [] + const displaySubBlocks: BlockSubBlockConfig[] = useSubBlocks + ? subBlocksResult!.subBlocks + : [] // Check if tool requires OAuth const requiresOAuth = @@ -2466,11 +1815,14 @@ export const ToolInput = memo(function ToolInput({ // Determine if tool has expandable body content const hasOperations = !isCustomTool && !isMcpTool && hasMultipleOperations(tool.type) - const filteredDisplayParams = displayParams.filter((param) => - evaluateParameterCondition(param, tool) - ) + // For subblock-based rendering, conditions are already evaluated in getSubBlocksForToolInput + const filteredDisplayParams = useSubBlocks + ? displayParams // unused when useSubBlocks, but needed for type consistency + : displayParams.filter((param) => evaluateParameterCondition(param, tool)) const hasToolBody = - hasOperations || (requiresOAuth && oauthConfig) || filteredDisplayParams.length > 0 + hasOperations || + (requiresOAuth && oauthConfig) || + (useSubBlocks ? displaySubBlocks.length > 0 : filteredDisplayParams.length > 0) // Only show expansion if tool has body content const isExpandedForDisplay = hasToolBody @@ -2695,96 +2047,105 @@ export const ToolInput = memo(function ToolInput({ {/* Tool parameters */} {(() => { + // SubBlock-first rendering for registry tools + if (useSubBlocks && displaySubBlocks.length > 0) { + return displaySubBlocks.map((sb) => { + const effectiveParamId = sb.canonicalParamId || sb.id + const visibility = + sb.paramVisibility || + toolParams?.allParameters?.find((p) => p.id === effectiveParamId) + ?.visibility || + 'user-or-llm' + const isRequired = sb.required === true + + // Compute canonical toggle for basic/advanced mode switching + const canonicalId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id] + const canonicalGroup = canonicalId + ? toolCanonicalIndex?.groupsById[canonicalId] + : undefined + const hasCanonicalPair = isCanonicalPair(canonicalGroup) + const canonicalMode = + canonicalGroup && hasCanonicalPair + ? resolveCanonicalMode( + canonicalGroup, + { operation: tool.operation, ...tool.params }, + canonicalModeOverrides + ) + : undefined + + const canonicalToggleProp = + hasCanonicalPair && canonicalMode && canonicalId + ? { + mode: canonicalMode, + disabled: disabled, + onToggle: () => { + const nextMode = + canonicalMode === 'advanced' ? 'basic' : 'advanced' + collaborativeSetBlockCanonicalMode(blockId, canonicalId, nextMode) + }, + } + : undefined + + return ( + + {(wandControlRef) => ( + + )} + + ) + }) + } + + // Fallback: legacy ToolParameterConfig-based rendering + // Used for custom tools, MCP tools, and registry tools without subBlocks const filteredParams = displayParams.filter((param) => evaluateParameterCondition(param, tool) ) - const groupedParams: { [key: string]: ToolParameterConfig[] } = {} - const standaloneParams: ToolParameterConfig[] = [] - - // Group checkbox-list parameters by their UI component title - filteredParams.forEach((param) => { - const paramConfig = param as ToolParameterConfig - if ( - paramConfig.uiComponent?.type === 'checkbox-list' && - paramConfig.uiComponent?.title - ) { - const groupKey = paramConfig.uiComponent.title - if (!groupedParams[groupKey]) { - groupedParams[groupKey] = [] - } - groupedParams[groupKey].push(paramConfig) - } else { - standaloneParams.push(paramConfig) - } - }) const renderedElements: React.ReactNode[] = [] - // Render grouped checkbox-lists - Object.entries(groupedParams).forEach(([groupTitle, params]) => { - const firstParam = params[0] as ToolParameterConfig - const groupValue = JSON.stringify( - params.reduce( - (acc, p) => ({ ...acc, [p.id]: tool.params?.[p.id] === 'true' }), - {} - ) - ) - - renderedElements.push( -
-
- {groupTitle} -
-
- { - try { - const parsed = JSON.parse(value) - params.forEach((param) => { - handleParamChange( - toolIndex, - param.id, - parsed[param.id] ? 'true' : 'false' - ) - }) - } catch (e) { - // Handle error - } - }} - uiComponent={firstParam.uiComponent} - disabled={disabled} - /> -
-
- ) - }) - - // Render standalone parameters - standaloneParams.forEach((param) => { + filteredParams.forEach((param) => { renderedElements.push( -
-
- {param.uiComponent?.title || formatParameterLabel(param.id)} - {param.required && param.visibility === 'user-only' && ( - * - )} - {param.visibility === 'user-or-llm' && ( - - (optional) - - )} -
-
- {param.uiComponent ? ( + + {(wandControlRef) => + param.uiComponent ? ( renderParameterInput( param, tool.params?.[param.id] || '', (value) => handleParamChange(toolIndex, param.id, value), toolIndex, - toolContextValues as Record + toolContextValues as Record, + wandControlRef ) ) : ( handleParamChange(toolIndex, param.id, value)} + wandControlRef={wandControlRef} + hideInternalWand={true} /> - )} -
-
+ ) + } + ) }) diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 08a716925f..8ac262bef5 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -196,6 +196,8 @@ export interface SubBlockConfig { type: SubBlockType mode?: 'basic' | 'advanced' | 'both' | 'trigger' // Default is 'both' if not specified. 'trigger' means only shown in trigger mode canonicalParamId?: string + /** Controls parameter visibility in agent/tool-input context */ + paramVisibility?: 'user-or-llm' | 'user-only' | 'llm-only' | 'hidden' required?: | boolean | { diff --git a/apps/sim/tools/params-resolver.ts b/apps/sim/tools/params-resolver.ts index 641511af6f..d213372d64 100644 --- a/apps/sim/tools/params-resolver.ts +++ b/apps/sim/tools/params-resolver.ts @@ -1,6 +1,7 @@ import { buildCanonicalIndex, type CanonicalIndex, + type CanonicalModeOverrides, evaluateSubBlockCondition, getCanonicalValues, isCanonicalPair, @@ -12,7 +13,10 @@ import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types' export { buildCanonicalIndex, type CanonicalIndex, + type CanonicalModeOverrides, evaluateSubBlockCondition, + isCanonicalPair, + resolveCanonicalMode, type SubBlockCondition, } diff --git a/apps/sim/tools/params.ts b/apps/sim/tools/params.ts index e1bb8fe7b5..8729acb84f 100644 --- a/apps/sim/tools/params.ts +++ b/apps/sim/tools/params.ts @@ -1,13 +1,17 @@ import { createLogger } from '@sim/logger' import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format' import { + buildCanonicalIndex, + type CanonicalModeOverrides, evaluateSubBlockCondition, + isCanonicalPair, + resolveCanonicalMode, type SubBlockCondition, } from '@/lib/workflows/subblocks/visibility' -import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types' +import type { SubBlockConfig as BlockSubBlockConfig, GenerationType } from '@/blocks/types' import { safeAssign } from '@/tools/safe-assign' import { isEmptyTagValue } from '@/tools/shared/tags' -import type { ParameterVisibility, ToolConfig } from '@/tools/types' +import type { OAuthConfig, ParameterVisibility, ToolConfig } from '@/tools/types' import { getTool } from '@/tools/utils' const logger = createLogger('ToolsParams') @@ -64,6 +68,14 @@ export interface UIComponentConfig { mode?: 'basic' | 'advanced' | 'both' | 'trigger' /** The actual subblock ID this config was derived from */ actualSubBlockId?: string + /** Wand configuration for AI assistance */ + wandConfig?: { + enabled: boolean + prompt: string + generationType?: GenerationType + placeholder?: string + maintainHistory?: boolean + } } export interface SubBlockConfig { @@ -327,6 +339,7 @@ export function getToolParametersConfig( canonicalParamId: subBlock.canonicalParamId, mode: subBlock.mode, actualSubBlockId: subBlock.id, + wandConfig: subBlock.wandConfig, } } } @@ -812,3 +825,196 @@ export function formatParameterLabel(paramId: string): string { // Simple case - just capitalize first letter return paramId.charAt(0).toUpperCase() + paramId.slice(1) } + +/** + * SubBlock IDs that are "structural" — they control tool routing or auth, + * not user-facing parameters. These are excluded from tool-input rendering + * unless they have an explicit paramVisibility set. + */ +const STRUCTURAL_SUBBLOCK_IDS = new Set(['operation', 'authMethod', 'destinationType']) + +/** + * SubBlock types that represent auth/credential inputs handled separately + * by the tool-input OAuth credential selector. + */ +const AUTH_SUBBLOCK_TYPES = new Set(['oauth-input']) + +/** + * SubBlock types that should never appear in tool-input context. + */ +const EXCLUDED_SUBBLOCK_TYPES = new Set([ + 'tool-input', + 'skill-input', + 'condition-input', + 'eval-input', + 'webhook-config', + 'schedule-info', + 'trigger-save', + 'input-format', + 'response-format', + 'mcp-server-selector', + 'mcp-tool-selector', + 'mcp-dynamic-args', + 'input-mapping', + 'variables-input', + 'messages-input', + 'router-input', + 'text', +]) + +export interface SubBlocksForToolInput { + toolConfig: ToolConfig + subBlocks: BlockSubBlockConfig[] + oauthConfig?: OAuthConfig +} + +/** + * Returns filtered SubBlockConfig[] for rendering in tool-input context. + * Uses subblock definitions as the primary source of UI metadata, + * getting all features (wandConfig, rich conditions, dependsOn, etc.) for free. + * + * For blocks without paramVisibility annotations, falls back to inferring + * visibility from the tool's param definitions. + */ +export function getSubBlocksForToolInput( + toolId: string, + blockType: string, + currentValues?: Record, + canonicalModeOverrides?: CanonicalModeOverrides +): SubBlocksForToolInput | null { + try { + const toolConfig = getTool(toolId) + if (!toolConfig) { + logger.warn(`Tool not found: ${toolId}`) + return null + } + + const blockConfigs = getBlockConfigurations() + const blockConfig = blockConfigs[blockType] + if (!blockConfig?.subBlocks?.length) { + return null + } + + const allSubBlocks = blockConfig.subBlocks as BlockSubBlockConfig[] + const canonicalIndex = buildCanonicalIndex(allSubBlocks) + + // Build values for condition evaluation + const values = currentValues || {} + const valuesWithOperation = { ...values } + if (valuesWithOperation.operation === undefined) { + const parts = toolId.split('_') + valuesWithOperation.operation = + parts.length >= 3 ? parts.slice(2).join('_') : parts[parts.length - 1] + } + + // Build a set of param IDs from the tool config for fallback visibility inference + const toolParamIds = new Set(Object.keys(toolConfig.params || {})) + const toolParamVisibility: Record = {} + for (const [paramId, param] of Object.entries(toolConfig.params || {})) { + toolParamVisibility[paramId] = + param.visibility ?? (param.required ? 'user-or-llm' : 'user-only') + } + + // Track which canonical groups we've already included (to avoid duplicates) + const includedCanonicalIds = new Set() + + const filtered: BlockSubBlockConfig[] = [] + + for (const sb of allSubBlocks) { + // Skip excluded types + if (EXCLUDED_SUBBLOCK_TYPES.has(sb.type)) continue + + // Skip trigger-mode-only subblocks + if (sb.mode === 'trigger') continue + + // Determine the effective param ID (canonical or subblock id) + const effectiveParamId = sb.canonicalParamId || sb.id + + // Resolve paramVisibility: explicit > inferred from tool params > skip + let visibility = sb.paramVisibility + if (!visibility) { + // Infer from structural checks + if (STRUCTURAL_SUBBLOCK_IDS.has(sb.id)) { + visibility = 'hidden' + } else if (AUTH_SUBBLOCK_TYPES.has(sb.type)) { + visibility = 'hidden' + } else if ( + sb.password && + (sb.id === 'botToken' || sb.id === 'accessToken' || sb.id === 'apiKey') + ) { + // Auth tokens without explicit paramVisibility are hidden + // (they're handled by the OAuth credential selector or structurally) + // But only if they don't have a matching tool param + if (!toolParamIds.has(sb.id)) { + visibility = 'hidden' + } else { + visibility = toolParamVisibility[sb.id] || 'user-or-llm' + } + } else if (toolParamIds.has(effectiveParamId)) { + // Fallback: infer from tool param visibility + visibility = toolParamVisibility[effectiveParamId] + } else if (toolParamIds.has(sb.id)) { + visibility = toolParamVisibility[sb.id] + } else { + // SubBlock has no corresponding tool param — skip it + continue + } + } + + // Filter by visibility: exclude hidden and llm-only + if (visibility === 'hidden' || visibility === 'llm-only') continue + + // Evaluate condition against current values + if (sb.condition) { + const conditionMet = evaluateSubBlockCondition( + sb.condition as SubBlockCondition, + valuesWithOperation + ) + if (!conditionMet) continue + } + + // Handle canonical pairs: only include the active mode variant + const canonicalId = canonicalIndex.canonicalIdBySubBlockId[sb.id] + if (canonicalId) { + const group = canonicalIndex.groupsById[canonicalId] + if (group && isCanonicalPair(group)) { + if (includedCanonicalIds.has(canonicalId)) continue + includedCanonicalIds.add(canonicalId) + + // Determine active mode + const mode = resolveCanonicalMode(group, valuesWithOperation, canonicalModeOverrides) + if (mode === 'advanced') { + // Find the advanced variant + const advancedSb = allSubBlocks.find((s) => group.advancedIds.includes(s.id)) + if (advancedSb) { + filtered.push(advancedSb) + } + } else { + // Include basic variant (current sb if it's the basic one) + if (group.basicId === sb.id) { + filtered.push(sb) + } else { + const basicSb = allSubBlocks.find((s) => s.id === group.basicId) + if (basicSb) { + filtered.push(basicSb) + } + } + } + continue + } + } + + // Non-canonical, non-hidden, condition-passing subblock + filtered.push(sb) + } + + return { + toolConfig, + subBlocks: filtered, + oauthConfig: toolConfig.oauth, + } + } catch (error) { + logger.error('Error getting subblocks for tool input:', error) + return null + } +} From a0ebe0842be6530e0be8804cae4e4bdcae0c7ea2 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 12 Feb 2026 00:39:22 -0800 Subject: [PATCH 02/17] fix(tool-input): restore optional indicator, fix folder selector and canonical toggle, extract components - Attach resolved paramVisibility to subblocks from getSubBlocksForToolInput - Add labelSuffix prop to SubBlock for "(optional)" badge on user-or-llm params - Fix folder selector missing for tools with canonicalParamId (e.g. Google Drive) - Fix canonical toggle not clickable by letting SubBlock handle dependsOn internally - Extract ParameterWithLabel, ToolSubBlockRenderer, ToolCredentialSelector to components/tools/ - Extract StoredTool interface to types.ts, selection helpers to utils.ts - Remove dead code (mcpError, refreshTools, oldParamIds, initialParams) - Strengthen typing: replace any with proper types on icon components and evaluateParameterCondition --- .../components/tool-sub-block-renderer.tsx | 410 -------- .../credential-selector.tsx} | 0 .../tool-input/components/tools/parameter.tsx | 189 ++++ .../components/tools/sub-block-renderer.tsx | 109 +++ .../components/tool-input/tool-input.test.ts | 37 +- .../components/tool-input/tool-input.tsx | 900 ++++++------------ .../sub-block/components/tool-input/types.ts | 31 + .../sub-block/components/tool-input/utils.ts | 32 + .../editor/components/sub-block/sub-block.tsx | 12 +- apps/sim/tools/params.ts | 22 +- 10 files changed, 696 insertions(+), 1046 deletions(-) delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-sub-block-renderer.tsx rename apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/{tool-credential-selector.tsx => tools/credential-selector.tsx} (100%) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/utils.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-sub-block-renderer.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-sub-block-renderer.tsx deleted file mode 100644 index bb8c26f9c5..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-sub-block-renderer.tsx +++ /dev/null @@ -1,410 +0,0 @@ -'use client' - -import type React from 'react' -import { useCallback, useEffect, useRef } from 'react' -import { Combobox, Switch } from '@/components/emcn' -import { - CheckboxList, - Code, - DocumentSelector, - DocumentTagEntry, - FileSelectorInput, - FileUpload, - FolderSelectorInput, - KnowledgeBaseSelector, - KnowledgeTagFilters, - LongInput, - ProjectSelectorInput, - SheetSelectorInput, - ShortInput, - SlackSelectorInput, - SliderInput, - Table, - TimeInput, - WorkflowSelectorInput, -} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components' -import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' -import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' -import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block' -import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types' -import { isPasswordParameter } from '@/tools/params' - -interface ToolSubBlockRendererProps { - blockId: string - subBlockId: string - toolIndex: number - subBlock: BlockSubBlockConfig - effectiveParamId: string - toolParams: Record | undefined - onParamChange: (toolIndex: number, paramId: string, value: string) => void - disabled: boolean - previewContextValues?: Record - wandControlRef?: React.MutableRefObject -} - -/** - * Renders a subblock component inside tool-input by bridging the subblock store - * with StoredTool.params via a synthetic store key. - * - * Replaces the 17+ individual SyncWrapper components that previously existed. - * Components read/write to the store at a synthetic ID, and two effects - * handle bidirectional sync with tool.params. - */ -export function ToolSubBlockRenderer({ - blockId, - subBlockId, - toolIndex, - subBlock, - effectiveParamId, - toolParams, - onParamChange, - disabled, - previewContextValues, - wandControlRef, -}: ToolSubBlockRendererProps) { - const syntheticId = `${subBlockId}-tool-${toolIndex}-${effectiveParamId}` - const [storeValue, setStoreValue] = useSubBlockValue(blockId, syntheticId) - - // Gate the component using the same dependsOn logic as SubBlock - const { finalDisabled } = useDependsOnGate(blockId, subBlock, { - disabled, - previewContextValues, - }) - - const toolParamValue = toolParams?.[effectiveParamId] ?? '' - - /** Tracks the last value we wrote to the store from tool.params to avoid echo loops */ - const lastInitRef = useRef(toolParamValue) - /** Tracks the last value we synced back to tool.params from the store */ - const lastSyncRef = useRef(toolParamValue) - - // Init effect: push tool.params value into the store when it changes externally - useEffect(() => { - if (toolParamValue !== lastInitRef.current) { - lastInitRef.current = toolParamValue - lastSyncRef.current = toolParamValue - setStoreValue(toolParamValue) - } - }, [toolParamValue, setStoreValue]) - - // Sync effect: when the store changes (user interaction), push back to tool.params - useEffect(() => { - if (storeValue == null) return - const stringValue = typeof storeValue === 'string' ? storeValue : JSON.stringify(storeValue) - if (stringValue !== lastSyncRef.current) { - lastSyncRef.current = stringValue - lastInitRef.current = stringValue - onParamChange(toolIndex, effectiveParamId, stringValue) - } - }, [storeValue, toolIndex, effectiveParamId, onParamChange]) - - // Initialize the store on first mount - const hasInitializedRef = useRef(false) - useEffect(() => { - if (!hasInitializedRef.current && toolParamValue) { - hasInitializedRef.current = true - setStoreValue(toolParamValue) - } - }, []) // eslint-disable-line react-hooks/exhaustive-deps - - const configWithSyntheticId = { ...subBlock, id: syntheticId } - - return renderSubBlockComponent({ - blockId, - syntheticId, - config: configWithSyntheticId, - subBlock, - disabled: finalDisabled, - previewContextValues, - wandControlRef, - toolParamValue, - onParamChange: useCallback( - (value: string) => onParamChange(toolIndex, effectiveParamId, value), - [toolIndex, effectiveParamId, onParamChange] - ), - }) -} - -interface RenderContext { - blockId: string - syntheticId: string - config: BlockSubBlockConfig - subBlock: BlockSubBlockConfig - disabled: boolean - previewContextValues?: Record - wandControlRef?: React.MutableRefObject - toolParamValue: string - onParamChange: (value: string) => void -} - -/** - * Renders the appropriate component for a subblock type. - * Mirrors the switch cases in SubBlock's renderInput(), using - * the same component props pattern. - */ -function renderSubBlockComponent(ctx: RenderContext): React.ReactNode { - const { - blockId, - syntheticId, - config, - subBlock, - disabled, - previewContextValues, - wandControlRef, - toolParamValue, - onParamChange, - } = ctx - - switch (subBlock.type) { - case 'short-input': - return ( - - ) - - case 'long-input': - return ( - - ) - - case 'dropdown': - return ( - option.id !== '') - .map((option) => ({ - label: option.label, - value: option.id, - })) || [] - } - value={toolParamValue} - onChange={onParamChange} - placeholder={subBlock.placeholder || 'Select option'} - disabled={disabled} - /> - ) - - case 'switch': - return ( - onParamChange(checked ? 'true' : 'false')} - /> - ) - - case 'code': - return ( - - ) - - case 'channel-selector': - case 'user-selector': - return ( - - ) - - case 'project-selector': - return ( - - ) - - case 'file-selector': - return ( - - ) - - case 'sheet-selector': - return ( - - ) - - case 'folder-selector': - return ( - - ) - - case 'knowledge-base-selector': - return - - case 'document-selector': - return ( - - ) - - case 'document-tag-entry': - return ( - - ) - - case 'knowledge-tag-filters': - return ( - - ) - - case 'table': - return ( -
- ) - - case 'slider': - return ( - - ) - - case 'checkbox-list': - return ( - - ) - - case 'time-input': - return ( - - ) - - case 'file-upload': - return ( - - ) - - case 'combobox': - return ( - ({ - label: opt.label, - value: opt.id, - }) - )} - value={toolParamValue} - onChange={onParamChange} - placeholder={subBlock.placeholder || 'Select option'} - disabled={disabled} - /> - ) - - case 'workflow-selector': - return - - case 'oauth-input': - // OAuth inputs are handled separately by ToolCredentialSelector in the parent - return null - - default: - return ( - - ) - } -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx similarity index 100% rename from apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector.tsx rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx new file mode 100644 index 0000000000..fe9ed32740 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx @@ -0,0 +1,189 @@ +'use client' + +import type React from 'react' +import { useRef, useState } from 'react' +import { ArrowLeftRight, ArrowUp } from 'lucide-react' +import { Button, Input, Label, Tooltip } from '@/components/emcn' +import { cn } from '@/lib/core/utils/cn' +import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block' + +/** + * Props for a generic parameter with label component + */ +export interface ParameterWithLabelProps { + paramId: string + title: string + isRequired: boolean + visibility: string + wandConfig?: { + enabled: boolean + prompt?: string + placeholder?: string + } + canonicalToggle?: { + mode: 'basic' | 'advanced' + disabled?: boolean + onToggle?: () => void + } + disabled: boolean + isPreview: boolean + children: (wandControlRef: React.MutableRefObject) => React.ReactNode +} + +/** + * Generic wrapper component for parameters that manages wand state and renders label + input + */ +export function ParameterWithLabel({ + paramId, + title, + isRequired, + visibility, + wandConfig, + canonicalToggle, + disabled, + isPreview, + children, +}: ParameterWithLabelProps) { + const [isSearchActive, setIsSearchActive] = useState(false) + const [searchQuery, setSearchQuery] = useState('') + const searchInputRef = useRef(null) + const wandControlRef = useRef(null) + + const isWandEnabled = wandConfig?.enabled ?? false + const showWand = isWandEnabled && !isPreview && !disabled + + const handleSearchClick = (): void => { + setIsSearchActive(true) + setTimeout(() => { + searchInputRef.current?.focus() + }, 0) + } + + const handleSearchBlur = (): void => { + if (!searchQuery.trim() && !wandControlRef.current?.isWandStreaming) { + setIsSearchActive(false) + } + } + + const handleSearchChange = (value: string): void => { + setSearchQuery(value) + } + + const handleSearchSubmit = (): void => { + if (searchQuery.trim() && wandControlRef.current) { + wandControlRef.current.onWandTrigger(searchQuery) + setSearchQuery('') + setIsSearchActive(false) + } + } + + const handleSearchCancel = (): void => { + setSearchQuery('') + setIsSearchActive(false) + } + + const isStreaming = wandControlRef.current?.isWandStreaming ?? false + + return ( +
+
+ +
+ {showWand && + (!isSearchActive ? ( + + ) : ( +
+ ) => + handleSearchChange(e.target.value) + } + onBlur={(e: React.FocusEvent) => { + const relatedTarget = e.relatedTarget as HTMLElement | null + if (relatedTarget?.closest('button')) return + handleSearchBlur() + }} + onKeyDown={(e: React.KeyboardEvent) => { + if (e.key === 'Enter' && searchQuery.trim() && !isStreaming) { + handleSearchSubmit() + } else if (e.key === 'Escape') { + handleSearchCancel() + } + }} + disabled={isStreaming} + className={cn( + 'h-5 min-w-[80px] flex-1 text-[11px]', + isStreaming && 'text-muted-foreground' + )} + placeholder='Generate with AI...' + /> + +
+ ))} + {canonicalToggle && !isPreview && ( + + + + + +

+ {canonicalToggle.mode === 'advanced' + ? 'Switch to selector' + : 'Switch to manual ID'} +

+
+
+ )} +
+
+
{children(wandControlRef)}
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx new file mode 100644 index 0000000000..a158cb7636 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx @@ -0,0 +1,109 @@ +'use client' + +import { useEffect, useMemo, useRef } from 'react' +import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' +import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block' +import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types' + +interface ToolSubBlockRendererProps { + blockId: string + subBlockId: string + toolIndex: number + subBlock: BlockSubBlockConfig + effectiveParamId: string + toolParams: Record | undefined + onParamChange: (toolIndex: number, paramId: string, value: string) => void + disabled: boolean + canonicalToggle?: { + mode: 'basic' | 'advanced' + disabled?: boolean + onToggle?: () => void + } +} + +/** + * Bridges the subblock store with StoredTool.params via a synthetic store key, + * then delegates all rendering to SubBlock for full parity. + * + * Two effects handle bidirectional sync: + * - tool.params → store (external changes) + * - store → tool.params (user interaction) + */ +export function ToolSubBlockRenderer({ + blockId, + subBlockId, + toolIndex, + subBlock, + effectiveParamId, + toolParams, + onParamChange, + disabled, + canonicalToggle, +}: ToolSubBlockRendererProps) { + const syntheticId = `${subBlockId}-tool-${toolIndex}-${effectiveParamId}` + const [storeValue, setStoreValue] = useSubBlockValue(blockId, syntheticId) + + const toolParamValue = toolParams?.[effectiveParamId] ?? '' + + /** Tracks the last value we pushed to the store from tool.params to avoid echo loops */ + const lastPushedToStoreRef = useRef(null) + /** Tracks the last value we synced back to tool.params from the store */ + const lastPushedToParamsRef = useRef(null) + + // Sync tool.params → store: push when the prop value changes (including first mount) + useEffect(() => { + if (!toolParamValue && lastPushedToStoreRef.current === null) { + // Skip initializing the store with an empty value on first mount — + // let the SubBlock component use its own default. + lastPushedToStoreRef.current = toolParamValue + lastPushedToParamsRef.current = toolParamValue + return + } + if (toolParamValue !== lastPushedToStoreRef.current) { + lastPushedToStoreRef.current = toolParamValue + lastPushedToParamsRef.current = toolParamValue + setStoreValue(toolParamValue) + } + }, [toolParamValue, setStoreValue]) + + // Sync store → tool.params: push when the user changes the value via SubBlock + useEffect(() => { + if (storeValue == null) return + const stringValue = typeof storeValue === 'string' ? storeValue : JSON.stringify(storeValue) + if (stringValue !== lastPushedToParamsRef.current) { + lastPushedToParamsRef.current = stringValue + lastPushedToStoreRef.current = stringValue + onParamChange(toolIndex, effectiveParamId, stringValue) + } + }, [storeValue, toolIndex, effectiveParamId, onParamChange]) + + // Determine if the parameter is optional for the user (LLM can fill it) + const visibility = subBlock.paramVisibility ?? 'user-or-llm' + const isOptionalForUser = visibility !== 'user-only' + + const labelSuffix = useMemo( + () => + isOptionalForUser ? ( + (optional) + ) : null, + [isOptionalForUser] + ) + + // Suppress SubBlock's "*" required indicator for optional-for-user params + const config = { + ...subBlock, + id: syntheticId, + ...(isOptionalForUser && { required: false }), + } + + return ( + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.test.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.test.ts index 8d2548c13b..44b73e1e4a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.test.ts @@ -2,37 +2,12 @@ * @vitest-environment node */ import { describe, expect, it } from 'vitest' - -interface StoredTool { - type: string - title?: string - toolId?: string - params?: Record - customToolId?: string - schema?: any - code?: string - operation?: string - usageControl?: 'auto' | 'force' | 'none' -} - -const isMcpToolAlreadySelected = (selectedTools: StoredTool[], mcpToolId: string): boolean => { - return selectedTools.some((tool) => tool.type === 'mcp' && tool.toolId === mcpToolId) -} - -const isCustomToolAlreadySelected = ( - selectedTools: StoredTool[], - customToolId: string -): boolean => { - return selectedTools.some( - (tool) => tool.type === 'custom-tool' && tool.customToolId === customToolId - ) -} - -const isWorkflowAlreadySelected = (selectedTools: StoredTool[], workflowId: string): boolean => { - return selectedTools.some( - (tool) => tool.type === 'workflow_input' && tool.params?.workflowId === workflowId - ) -} +import type { StoredTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types' +import { + isCustomToolAlreadySelected, + isMcpToolAlreadySelected, + isWorkflowAlreadySelected, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/utils' describe('isMcpToolAlreadySelected', () => { describe('basic functionality', () => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index 49ba83688b..b35d59e648 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -1,16 +1,13 @@ import type React from 'react' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import { ArrowLeftRight, ArrowUp, Loader2, WrenchIcon, XIcon } from 'lucide-react' +import { Loader2, WrenchIcon, XIcon } from 'lucide-react' import { useParams } from 'next/navigation' import { Badge, - Button, Combobox, type ComboboxOption, type ComboboxOptionGroup, - Input, - Label, Popover, PopoverContent, PopoverItem, @@ -26,6 +23,7 @@ import { isToolUnavailable, getMcpToolIssue as validateMcpTool, } from '@/lib/mcp/tool-validation' +import type { McpToolSchema } from '@/lib/mcp/types' import { getCanonicalScopesForProvider, getProviderIdFromServiceId, @@ -42,8 +40,15 @@ import { type CustomTool, CustomToolModal, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal' -import { ToolCredentialSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector' -import { ToolSubBlockRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-sub-block-renderer' +import { ToolCredentialSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector' +import { ParameterWithLabel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter' +import { ToolSubBlockRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer' +import type { StoredTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types' +import { + isCustomToolAlreadySelected, + isMcpToolAlreadySelected, + isWorkflowAlreadySelected, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/utils' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block' import { getAllBlocks } from '@/blocks' @@ -92,281 +97,6 @@ import { const logger = createLogger('ToolInput') -/** - * Props for a generic parameter with label component - */ -interface ParameterWithLabelProps { - paramId: string - title: string - isRequired: boolean - visibility: string - wandConfig?: { - enabled: boolean - prompt?: string - placeholder?: string - } - canonicalToggle?: { - mode: 'basic' | 'advanced' - disabled?: boolean - onToggle?: () => void - } - disabled: boolean - isPreview: boolean - children: (wandControlRef: React.MutableRefObject) => React.ReactNode -} - -/** - * Generic wrapper component for parameters that manages wand state and renders label + input - */ -const ParameterWithLabel: React.FC = ({ - paramId, - title, - isRequired, - visibility, - wandConfig, - canonicalToggle, - disabled, - isPreview, - children, -}) => { - const [isSearchActive, setIsSearchActive] = useState(false) - const [searchQuery, setSearchQuery] = useState('') - const searchInputRef = useRef(null) - const wandControlRef = useRef(null) - - const isWandEnabled = wandConfig?.enabled ?? false - const showWand = isWandEnabled && !isPreview && !disabled - - const handleSearchClick = (): void => { - setIsSearchActive(true) - setTimeout(() => { - searchInputRef.current?.focus() - }, 0) - } - - const handleSearchBlur = (): void => { - if (!searchQuery.trim() && !wandControlRef.current?.isWandStreaming) { - setIsSearchActive(false) - } - } - - const handleSearchChange = (value: string): void => { - setSearchQuery(value) - } - - const handleSearchSubmit = (): void => { - if (searchQuery.trim() && wandControlRef.current) { - wandControlRef.current.onWandTrigger(searchQuery) - setSearchQuery('') - setIsSearchActive(false) - } - } - - const handleSearchCancel = (): void => { - setSearchQuery('') - setIsSearchActive(false) - } - - const isStreaming = wandControlRef.current?.isWandStreaming ?? false - - return ( -
-
- -
- {showWand && ( - <> - {!isSearchActive ? ( - - ) : ( -
- ) => - handleSearchChange(e.target.value) - } - onBlur={(e: React.FocusEvent) => { - const relatedTarget = e.relatedTarget as HTMLElement | null - if (relatedTarget?.closest('button')) return - handleSearchBlur() - }} - onKeyDown={(e: React.KeyboardEvent) => { - if (e.key === 'Enter' && searchQuery.trim() && !isStreaming) { - handleSearchSubmit() - } else if (e.key === 'Escape') { - handleSearchCancel() - } - }} - disabled={isStreaming} - className={cn( - 'h-5 min-w-[80px] flex-1 text-[11px]', - isStreaming && 'text-muted-foreground' - )} - placeholder='Generate with AI...' - /> - -
- )} - - )} - {canonicalToggle && !isPreview && ( - - - - - -

- {canonicalToggle.mode === 'advanced' - ? 'Switch to selector' - : 'Switch to manual ID'} -

-
-
- )} -
-
-
{children(wandControlRef)}
-
- ) -} - -/** - * Props for the ToolInput component - */ -interface ToolInputProps { - /** Unique identifier for the block */ - blockId: string - /** Unique identifier for the sub-block */ - subBlockId: string - /** Whether component is in preview mode */ - isPreview?: boolean - /** Value to display in preview mode */ - previewValue?: any - /** Whether the input is disabled */ - disabled?: boolean - /** Allow expanding tools in preview mode */ - allowExpandInPreview?: boolean -} - -/** - * Represents a tool selected and configured in the workflow - * - * @remarks - * For custom tools (new format), we only store: type, customToolId, usageControl, isExpanded. - * Everything else (title, schema, code) is loaded dynamically from the database. - * Legacy custom tools with inline schema/code are still supported for backwards compatibility. - */ -interface StoredTool { - /** Block type identifier */ - type: string - /** Display title for the tool (optional for new custom tool format) */ - title?: string - /** Direct tool ID for execution (optional for new custom tool format) */ - toolId?: string - /** Parameter values configured by the user (optional for new custom tool format) */ - params?: Record - /** Whether the tool details are expanded in UI */ - isExpanded?: boolean - /** Database ID for custom tools (new format - reference only) */ - customToolId?: string - /** Tool schema for custom tools (legacy format - inline) */ - schema?: any - /** Implementation code for custom tools (legacy format - inline) */ - code?: string - /** Selected operation for multi-operation tools */ - operation?: string - /** Tool usage control mode for LLM */ - usageControl?: 'auto' | 'force' | 'none' -} - -/** - * Resolves a custom tool reference to its full definition. - * - * @remarks - * Custom tools can be stored in two formats: - * 1. Reference-only (new): `{ customToolId: "...", usageControl: "auto" }` - loads from database - * 2. Inline (legacy): `{ schema: {...}, code: "..." }` - uses embedded definition - * - * @param storedTool - The stored tool reference containing either a customToolId or inline definition - * @param customToolsList - List of custom tools fetched from the database - * @returns The resolved custom tool with schema, code, and title, or `null` if not found - */ -function resolveCustomToolFromReference( - storedTool: StoredTool, - customToolsList: CustomToolDefinition[] -): { schema: any; code: string; title: string } | null { - // If the tool has a customToolId (new reference format), look it up - if (storedTool.customToolId) { - const customTool = customToolsList.find((t) => t.id === storedTool.customToolId) - if (customTool) { - return { - schema: customTool.schema, - code: customTool.code, - title: customTool.title, - } - } - // If not found by ID, fall through to try other methods - logger.warn(`Custom tool not found by ID: ${storedTool.customToolId}`) - } - - // Legacy format: inline schema and code - if (storedTool.schema && storedTool.code !== undefined) { - return { - schema: storedTool.schema, - code: storedTool.code, - title: storedTool.title || '', - } - } - - return null -} - /** * Renders the input for workflow_executor's inputMapping parameter. * This is a special case that doesn't map to any SubBlockConfig, so it's kept here. @@ -525,6 +255,66 @@ function WorkflowToolDeployBadge({ ) } +/** + * Props for the ToolInput component + */ +interface ToolInputProps { + /** Unique identifier for the block */ + blockId: string + /** Unique identifier for the sub-block */ + subBlockId: string + /** Whether component is in preview mode */ + isPreview?: boolean + /** Value to display in preview mode */ + previewValue?: any + /** Whether the input is disabled */ + disabled?: boolean + /** Allow expanding tools in preview mode */ + allowExpandInPreview?: boolean +} + +/** + * Resolves a custom tool reference to its full definition. + * + * @remarks + * Custom tools can be stored in two formats: + * 1. Reference-only (new): `{ customToolId: "...", usageControl: "auto" }` - loads from database + * 2. Inline (legacy): `{ schema: {...}, code: "..." }` - uses embedded definition + * + * @param storedTool - The stored tool reference containing either a customToolId or inline definition + * @param customToolsList - List of custom tools fetched from the database + * @returns The resolved custom tool with schema, code, and title, or `null` if not found + */ +function resolveCustomToolFromReference( + storedTool: StoredTool, + customToolsList: CustomToolDefinition[] +): { schema: any; code: string; title: string } | null { + // If the tool has a customToolId (new reference format), look it up + if (storedTool.customToolId) { + const customTool = customToolsList.find((t) => t.id === storedTool.customToolId) + if (customTool) { + return { + schema: customTool.schema, + code: customTool.code, + title: customTool.title, + } + } + // If not found by ID, fall through to try other methods + logger.warn(`Custom tool not found by ID: ${storedTool.customToolId}`) + } + + // Legacy format: inline schema and code + if (storedTool.schema && storedTool.code !== undefined) { + return { + schema: storedTool.schema, + code: storedTool.code, + title: storedTool.title || '', + } + } + + return null +} + /** * Set of built-in tool types that are core platform tools. * @@ -557,7 +347,10 @@ const BUILT_IN_TOOL_TYPES = new Set([ * @param IconComponent - The Lucide icon component to render * @returns A styled div containing the icon with consistent dimensions */ -function createToolIcon(bgColor: string, IconComponent: any) { +function createToolIcon( + bgColor: string, + IconComponent: React.ComponentType<{ className?: string }> +) { return (
st.serverId === serverId && st.toolName === toolName) // Use DB schema if available, otherwise use Zustand schema - const schema = storedTool?.schema ?? tool.schema + const schema = storedTool?.schema ?? (tool.schema as McpToolSchema | undefined) return validateMcpTool( { @@ -825,40 +613,6 @@ export const ToolInput = memo(function ToolInput({ return selectedTools.some((tool) => tool.toolId === toolId) } - /** - * Checks if an MCP tool is already selected. - * - * @param mcpToolId - The MCP tool identifier to check - * @returns `true` if the MCP tool is already selected - */ - const isMcpToolAlreadySelected = (mcpToolId: string): boolean => { - return selectedTools.some((tool) => tool.type === 'mcp' && tool.toolId === mcpToolId) - } - - /** - * Checks if a custom tool is already selected. - * - * @param customToolId - The custom tool identifier to check - * @returns `true` if the custom tool is already selected - */ - const isCustomToolAlreadySelected = (customToolId: string): boolean => { - return selectedTools.some( - (tool) => tool.type === 'custom-tool' && tool.customToolId === customToolId - ) - } - - /** - * Checks if a workflow is already selected. - * - * @param workflowId - The workflow identifier to check - * @returns `true` if the workflow is already selected - */ - const isWorkflowAlreadySelected = (workflowId: string): boolean => { - return selectedTools.some( - (tool) => tool.type === 'workflow_input' && tool.params?.workflowId === workflowId - ) - } - /** * Checks if a block supports multiple operations. * @@ -1112,7 +866,7 @@ export const ToolInput = memo(function ToolInput({ customTools.some( (customTool) => customTool.id === toolId && - customTool.schema?.function?.name === tool.schema.function.name + customTool.schema?.function?.name === tool.schema?.function?.name ) ) { return false @@ -1168,10 +922,6 @@ export const ToolInput = memo(function ToolInput({ return } - const initialParams: Record = {} - - const oldToolParams = tool.toolId ? getToolParametersConfig(tool.toolId, tool.type) : null - const oldParamIds = new Set(oldToolParams?.userInputParameters.map((p) => p.id) || []) const newParamIds = new Set(toolParams.userInputParameters.map((p) => p.id)) const preservedParams: Record = {} @@ -1197,7 +947,7 @@ export const ToolInput = memo(function ToolInput({ ...tool, toolId: newToolId, operation, - params: { ...initialParams, ...preservedParams }, // Preserve all compatible existing values + params: preservedParams, } : tool ) @@ -1298,41 +1048,210 @@ export const ToolInput = memo(function ToolInput({ setDragOverIndex(null) } - const IconComponent = ({ icon: Icon, className }: { icon: any; className?: string }) => { + const IconComponent = ({ + icon: Icon, + className, + }: { + icon?: React.ComponentType<{ className?: string }> + className?: string + }) => { if (!Icon) return null return } + const evaluateParameterCondition = (param: ToolParameterConfig, tool: StoredTool): boolean => { + if (!('uiComponent' in param) || !param.uiComponent?.condition) return true + const currentValues: Record = { operation: tool.operation, ...tool.params } + return evaluateSubBlockCondition( + param.uiComponent.condition as SubBlockCondition, + currentValues + ) + } + /** - * Generates grouped options for the tool selection combobox. - * - * @remarks - * Groups tools into categories: Actions (create/add), Custom Tools, - * MCP Tools, Built-in Tools, and Integrations. + * Renders a parameter input for custom tools, MCP tools, and legacy registry + * tools that don't have SubBlockConfig definitions. * - * @returns Array of option groups for the combobox component + * Registry tools with subBlocks use ToolSubBlockRenderer instead. */ - const toolGroups = useMemo((): ComboboxOptionGroup[] => { - const groups: ComboboxOptionGroup[] = [] + const renderParameterInput = ( + param: ToolParameterConfig, + value: string, + onChange: (value: string) => void, + toolIndex?: number, + currentToolParams?: Record, + wandControlRef?: React.MutableRefObject + ) => { + const uniqueSubBlockId = + toolIndex !== undefined + ? `${subBlockId}-tool-${toolIndex}-${param.id}` + : `${subBlockId}-${param.id}` + const uiComponent = param.uiComponent - // Actions group (no section header) - const actionItems: ComboboxOption[] = [] - if (!permissionConfig.disableCustomTools) { - actionItems.push({ - label: 'Create Tool', - value: 'action-create-tool', - icon: WrenchIcon, - onSelect: () => { - setCustomToolModalOpen(true) - setOpen(false) - }, - disabled: isPreview, - }) + if (!uiComponent) { + return ( + + ) } - if (!permissionConfig.disableMcpTools) { - actionItems.push({ - label: 'Add MCP Server', - value: 'action-add-mcp', + + switch (uiComponent.type) { + case 'dropdown': + return ( + (option.id ?? option.value) !== '') + .map((option) => ({ + label: option.label, + value: option.id ?? option.value ?? '', + })) || [] + } + value={value} + onChange={onChange} + placeholder={uiComponent.placeholder || 'Select option'} + disabled={disabled} + /> + ) + + case 'switch': + return ( + onChange(checked ? 'true' : 'false')} + /> + ) + + case 'long-input': + return ( + + ) + + case 'short-input': + return ( + + ) + + case 'oauth-input': + return ( + + ) + + case 'workflow-input-mapper': { + const selectedWorkflowId = currentToolParams?.workflowId || '' + return ( + + ) + } + + default: + return ( + + ) + } + } + + /** + * Generates grouped options for the tool selection combobox. + * + * @remarks + * Groups tools into categories: Actions (create/add), Custom Tools, + * MCP Tools, Built-in Tools, and Integrations. + * + * @returns Array of option groups for the combobox component + */ + const toolGroups = useMemo((): ComboboxOptionGroup[] => { + const groups: ComboboxOptionGroup[] = [] + + // Actions group (no section header) + const actionItems: ComboboxOption[] = [] + if (!permissionConfig.disableCustomTools) { + actionItems.push({ + label: 'Create Tool', + value: 'action-create-tool', + icon: WrenchIcon, + onSelect: () => { + setCustomToolModalOpen(true) + setOpen(false) + }, + disabled: isPreview, + }) + } + if (!permissionConfig.disableMcpTools) { + actionItems.push({ + label: 'Add MCP Server', + value: 'action-add-mcp', icon: McpIcon, onSelect: () => { setOpen(false) @@ -1350,7 +1269,7 @@ export const ToolInput = memo(function ToolInput({ groups.push({ section: 'Custom Tools', items: customTools.map((customTool) => { - const alreadySelected = isCustomToolAlreadySelected(customTool.id) + const alreadySelected = isCustomToolAlreadySelected(selectedTools, customTool.id) return { label: customTool.title, value: `custom-${customTool.id}`, @@ -1381,7 +1300,7 @@ export const ToolInput = memo(function ToolInput({ section: 'MCP Tools', items: availableMcpTools.map((mcpTool) => { const server = mcpServers.find((s) => s.id === mcpTool.serverId) - const alreadySelected = isMcpToolAlreadySelected(mcpTool.id) + const alreadySelected = isMcpToolAlreadySelected(selectedTools, mcpTool.id) return { label: mcpTool.name, value: `mcp-${mcpTool.id}`, @@ -1458,7 +1377,7 @@ export const ToolInput = memo(function ToolInput({ groups.push({ section: 'Workflows', items: availableWorkflows.map((workflow) => { - const alreadySelected = isWorkflowAlreadySelected(workflow.id) + const alreadySelected = isWorkflowAlreadySelected(selectedTools, workflow.id) return { label: workflow.name, value: `workflow-${workflow.id}`, @@ -1504,9 +1423,6 @@ export const ToolInput = memo(function ToolInput({ availableWorkflows, getToolIdForOperation, isToolAlreadySelected, - isMcpToolAlreadySelected, - isCustomToolAlreadySelected, - isWorkflowAlreadySelected, ]) const toolRequiresOAuth = (toolId: string): boolean => { @@ -1519,169 +1435,6 @@ export const ToolInput = memo(function ToolInput({ return toolParams?.toolConfig?.oauth } - const evaluateParameterCondition = (param: any, tool: StoredTool): boolean => { - if (!('uiComponent' in param) || !param.uiComponent?.condition) return true - const currentValues: Record = { operation: tool.operation, ...tool.params } - return evaluateSubBlockCondition( - param.uiComponent.condition as SubBlockCondition, - currentValues - ) - } - - /** - * Renders a parameter input for custom tools, MCP tools, and legacy registry - * tools that don't have SubBlockConfig definitions. - * - * Registry tools with subBlocks use ToolSubBlockRenderer instead. - */ - const renderParameterInput = ( - param: ToolParameterConfig, - value: string, - onChange: (value: string) => void, - toolIndex?: number, - currentToolParams?: Record, - wandControlRef?: React.MutableRefObject - ) => { - const uniqueSubBlockId = - toolIndex !== undefined - ? `${subBlockId}-tool-${toolIndex}-${param.id}` - : `${subBlockId}-${param.id}` - const uiComponent = param.uiComponent - - if (!uiComponent) { - return ( - - ) - } - - switch (uiComponent.type) { - case 'dropdown': - return ( - (option.id ?? option.value) !== '') - .map((option) => ({ - label: option.label, - value: option.id ?? option.value ?? '', - })) || [] - } - value={value} - onChange={onChange} - placeholder={uiComponent.placeholder || 'Select option'} - disabled={disabled} - /> - ) - - case 'switch': - return ( - onChange(checked ? 'true' : 'false')} - /> - ) - - case 'long-input': - return ( - - ) - - case 'short-input': - return ( - - ) - - case 'oauth-input': - return ( - - ) - - case 'workflow-input-mapper': { - const selectedWorkflowId = currentToolParams?.workflowId || '' - return ( - - ) - } - - default: - return ( - - ) - } - } - return (
{/* Add Tool Combobox - always at top */} @@ -1815,14 +1568,10 @@ export const ToolInput = memo(function ToolInput({ // Determine if tool has expandable body content const hasOperations = !isCustomTool && !isMcpTool && hasMultipleOperations(tool.type) - // For subblock-based rendering, conditions are already evaluated in getSubBlocksForToolInput - const filteredDisplayParams = useSubBlocks - ? displayParams // unused when useSubBlocks, but needed for type consistency - : displayParams.filter((param) => evaluateParameterCondition(param, tool)) - const hasToolBody = - hasOperations || - (requiresOAuth && oauthConfig) || - (useSubBlocks ? displaySubBlocks.length > 0 : filteredDisplayParams.length > 0) + const hasParams = useSubBlocks + ? displaySubBlocks.length > 0 + : displayParams.filter((param) => evaluateParameterCondition(param, tool)).length > 0 + const hasToolBody = hasOperations || (requiresOAuth && oauthConfig) || hasParams // Only show expansion if tool has body content const isExpandedForDisplay = hasToolBody @@ -2030,7 +1779,9 @@ export const ToolInput = memo(function ToolInput({
handleParamChange(toolIndex, 'credential', value)} + onChange={(value: string) => + handleParamChange(toolIndex, 'credential', value) + } provider={oauthConfig.provider as OAuthProvider} requiredScopes={ toolBlock?.subBlocks?.find((sb) => sb.id === 'credential') @@ -2051,12 +1802,6 @@ export const ToolInput = memo(function ToolInput({ if (useSubBlocks && displaySubBlocks.length > 0) { return displaySubBlocks.map((sb) => { const effectiveParamId = sb.canonicalParamId || sb.id - const visibility = - sb.paramVisibility || - toolParams?.allParameters?.find((p) => p.id === effectiveParamId) - ?.visibility || - 'user-or-llm' - const isRequired = sb.required === true // Compute canonical toggle for basic/advanced mode switching const canonicalId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id] @@ -2077,7 +1822,6 @@ export const ToolInput = memo(function ToolInput({ hasCanonicalPair && canonicalMode && canonicalId ? { mode: canonicalMode, - disabled: disabled, onToggle: () => { const nextMode = canonicalMode === 'advanced' ? 'basic' : 'advanced' @@ -2086,33 +1830,24 @@ export const ToolInput = memo(function ToolInput({ } : undefined + // Ensure title is present for SubBlock's label rendering + const sbWithTitle = sb.title + ? sb + : { ...sb, title: formatParameterLabel(effectiveParamId) } + return ( - - {(wandControlRef) => ( - - )} - + canonicalToggle={canonicalToggleProp} + /> ) }) } @@ -2137,35 +1872,14 @@ export const ToolInput = memo(function ToolInput({ disabled={disabled} isPreview={isPreview || false} > - {(wandControlRef) => - param.uiComponent ? ( - renderParameterInput( - param, - tool.params?.[param.id] || '', - (value) => handleParamChange(toolIndex, param.id, value), - toolIndex, - toolContextValues as Record, - wandControlRef - ) - ) : ( - handleParamChange(toolIndex, param.id, value)} - wandControlRef={wandControlRef} - hideInternalWand={true} - /> + {(wandControlRef: React.MutableRefObject) => + renderParameterInput( + param, + tool.params?.[param.id] || '', + (value) => handleParamChange(toolIndex, param.id, value), + toolIndex, + toolContextValues as Record, + wandControlRef ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types.ts new file mode 100644 index 0000000000..138b6a5621 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types.ts @@ -0,0 +1,31 @@ +/** + * Represents a tool selected and configured in the workflow + * + * @remarks + * For custom tools (new format), we only store: type, customToolId, usageControl, isExpanded. + * Everything else (title, schema, code) is loaded dynamically from the database. + * Legacy custom tools with inline schema/code are still supported for backwards compatibility. + */ +export interface StoredTool { + /** Block type identifier */ + type: string + /** Display title for the tool (optional for new custom tool format) */ + title?: string + /** Direct tool ID for execution (optional for new custom tool format) */ + toolId?: string + /** Parameter values configured by the user (optional for new custom tool format) */ + params?: Record + /** Whether the tool details are expanded in UI */ + isExpanded?: boolean + /** Database ID for custom tools (new format - reference only) */ + customToolId?: string + /** Tool schema for custom tools (legacy format - inline JSON schema) */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema?: Record + /** Implementation code for custom tools (legacy format - inline) */ + code?: string + /** Selected operation for multi-operation tools */ + operation?: string + /** Tool usage control mode for LLM */ + usageControl?: 'auto' | 'force' | 'none' +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/utils.ts new file mode 100644 index 0000000000..1110a5808b --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/utils.ts @@ -0,0 +1,32 @@ +import type { StoredTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/types' + +/** + * Checks if an MCP tool is already selected. + */ +export function isMcpToolAlreadySelected(selectedTools: StoredTool[], mcpToolId: string): boolean { + return selectedTools.some((tool) => tool.type === 'mcp' && tool.toolId === mcpToolId) +} + +/** + * Checks if a custom tool is already selected. + */ +export function isCustomToolAlreadySelected( + selectedTools: StoredTool[], + customToolId: string +): boolean { + return selectedTools.some( + (tool) => tool.type === 'custom-tool' && tool.customToolId === customToolId + ) +} + +/** + * Checks if a workflow is already selected. + */ +export function isWorkflowAlreadySelected( + selectedTools: StoredTool[], + workflowId: string +): boolean { + return selectedTools.some( + (tool) => tool.type === 'workflow_input' && tool.params?.workflowId === workflowId + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index c8422f0e7c..edf99ce01a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -76,6 +76,7 @@ interface SubBlockProps { disabled?: boolean onToggle?: () => void } + labelSuffix?: React.ReactNode } /** @@ -202,7 +203,8 @@ const renderLabel = ( showCopyButton: boolean copied: boolean onCopy: () => void - } + }, + labelSuffix?: React.ReactNode ): JSX.Element | null => { if (config.type === 'switch') return null if (!config.title) return null @@ -218,6 +220,7 @@ const renderLabel = (
diff --git a/apps/sim/tools/params.ts b/apps/sim/tools/params.ts index 8729acb84f..89a9d0f8dc 100644 --- a/apps/sim/tools/params.ts +++ b/apps/sim/tools/params.ts @@ -907,8 +907,7 @@ export function getSubBlocksForToolInput( parts.length >= 3 ? parts.slice(2).join('_') : parts[parts.length - 1] } - // Build a set of param IDs from the tool config for fallback visibility inference - const toolParamIds = new Set(Object.keys(toolConfig.params || {})) + // Build a map of tool param IDs to their resolved visibility const toolParamVisibility: Record = {} for (const [paramId, param] of Object.entries(toolConfig.params || {})) { toolParamVisibility[paramId] = @@ -945,16 +944,21 @@ export function getSubBlocksForToolInput( // Auth tokens without explicit paramVisibility are hidden // (they're handled by the OAuth credential selector or structurally) // But only if they don't have a matching tool param - if (!toolParamIds.has(sb.id)) { + if (!(sb.id in toolParamVisibility)) { visibility = 'hidden' } else { visibility = toolParamVisibility[sb.id] || 'user-or-llm' } - } else if (toolParamIds.has(effectiveParamId)) { + } else if (effectiveParamId in toolParamVisibility) { // Fallback: infer from tool param visibility visibility = toolParamVisibility[effectiveParamId] - } else if (toolParamIds.has(sb.id)) { + } else if (sb.id in toolParamVisibility) { visibility = toolParamVisibility[sb.id] + } else if (sb.canonicalParamId) { + // SubBlock has a canonicalParamId that doesn't directly match a tool param. + // This means the block's params() function transforms it before sending to the tool + // (e.g. listFolderId → folderId). These are user-facing inputs, default to user-or-llm. + visibility = 'user-or-llm' } else { // SubBlock has no corresponding tool param — skip it continue @@ -987,16 +991,16 @@ export function getSubBlocksForToolInput( // Find the advanced variant const advancedSb = allSubBlocks.find((s) => group.advancedIds.includes(s.id)) if (advancedSb) { - filtered.push(advancedSb) + filtered.push({ ...advancedSb, paramVisibility: visibility }) } } else { // Include basic variant (current sb if it's the basic one) if (group.basicId === sb.id) { - filtered.push(sb) + filtered.push({ ...sb, paramVisibility: visibility }) } else { const basicSb = allSubBlocks.find((s) => s.id === group.basicId) if (basicSb) { - filtered.push(basicSb) + filtered.push({ ...basicSb, paramVisibility: visibility }) } } } @@ -1005,7 +1009,7 @@ export function getSubBlocksForToolInput( } // Non-canonical, non-hidden, condition-passing subblock - filtered.push(sb) + filtered.push({ ...sb, paramVisibility: visibility }) } return { From 8af561782d5e8cede8245b4447217318e2d57d8a Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 12 Feb 2026 10:34:25 -0800 Subject: [PATCH 03/17] add sibling values to subblock context since subblock store isn't relevant in tool input, and removed unused param --- .../components/tools/sub-block-renderer.tsx | 1 + .../editor/components/sub-block/sub-block.tsx | 61 +++++++++---------- .../panel/components/editor/editor.tsx | 2 - 3 files changed, 29 insertions(+), 35 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx index a158cb7636..177dd55a0e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx @@ -104,6 +104,7 @@ export function ToolSubBlockRenderer({ disabled={disabled} canonicalToggle={canonicalToggle} labelSuffix={labelSuffix} + dependencyContext={toolParams} /> ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index edf99ce01a..1cc690c3d7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -3,7 +3,6 @@ import { isEqual } from 'lodash' import { AlertTriangle, ArrowLeftRight, ArrowUp, Check, Clipboard } from 'lucide-react' import { Button, Input, Label, Tooltip } from '@/components/emcn/components' import { cn } from '@/lib/core/utils/cn' -import type { FieldDiffStatus } from '@/lib/workflows/diff/types' import { CheckboxList, Code, @@ -69,7 +68,6 @@ interface SubBlockProps { isPreview?: boolean subBlockValues?: Record disabled?: boolean - fieldDiffStatus?: FieldDiffStatus allowExpandInPreview?: boolean canonicalToggle?: { mode: 'basic' | 'advanced' @@ -77,6 +75,8 @@ interface SubBlockProps { onToggle?: () => void } labelSuffix?: React.ReactNode + /** Provides sibling values for dependency resolution in non-preview contexts (e.g. tool-input) */ + dependencyContext?: Record } /** @@ -163,16 +163,14 @@ const getPreviewValue = ( /** * Renders the label with optional validation and description tooltips. * - * @remarks - * Handles JSON validation indicators for code blocks and required field markers. - * Includes inline AI generate button when wand is enabled. - * * @param config - The sub-block configuration defining the label content * @param isValidJson - Whether the JSON content is valid (for code blocks) * @param subBlockValues - Current values of all subblocks for evaluating conditional requirements - * @param wandState - Optional state and handlers for the AI wand feature - * @param canonicalToggle - Optional canonical toggle metadata and handlers - * @param canonicalToggleIsDisabled - Whether the canonical toggle is disabled + * @param wandState - State and handlers for the inline AI generate feature + * @param canonicalToggle - Metadata and handlers for the basic/advanced mode toggle + * @param canonicalToggleIsDisabled - Whether the canonical toggle is disabled (includes dependsOn gating) + * @param copyState - State and handler for the copy-to-clipboard button + * @param labelSuffix - Additional content rendered after the label text * @returns The label JSX element, or `null` for switch types or when no title is defined */ const renderLabel = ( @@ -386,29 +384,25 @@ const arePropsEqual = (prevProps: SubBlockProps, nextProps: SubBlockProps): bool prevProps.isPreview === nextProps.isPreview && valueEqual && prevProps.disabled === nextProps.disabled && - prevProps.fieldDiffStatus === nextProps.fieldDiffStatus && prevProps.allowExpandInPreview === nextProps.allowExpandInPreview && canonicalToggleEqual && - prevProps.labelSuffix === nextProps.labelSuffix + prevProps.labelSuffix === nextProps.labelSuffix && + prevProps.dependencyContext === nextProps.dependencyContext ) } /** * Renders a single workflow sub-block input based on config.type. * - * @remarks - * Supports multiple input types including short-input, long-input, dropdown, - * combobox, slider, table, code, switch, tool-input, and many more. - * Handles preview mode, disabled states, and AI wand generation. - * * @param blockId - The parent block identifier * @param config - Configuration defining the input type and properties * @param isPreview - Whether to render in preview mode * @param subBlockValues - Current values of all subblocks * @param disabled - Whether the input is disabled - * @param fieldDiffStatus - Optional diff status for visual indicators * @param allowExpandInPreview - Whether to allow expanding in preview mode - * @returns The rendered sub-block input component + * @param canonicalToggle - Metadata and handlers for the basic/advanced mode toggle + * @param labelSuffix - Additional content rendered after the label text + * @param dependencyContext - Sibling values for dependency resolution in non-preview contexts (e.g. tool-input) */ function SubBlockComponent({ blockId, @@ -416,10 +410,10 @@ function SubBlockComponent({ isPreview = false, subBlockValues, disabled = false, - fieldDiffStatus, allowExpandInPreview, canonicalToggle, labelSuffix, + dependencyContext, }: SubBlockProps): JSX.Element { const [isValidJson, setIsValidJson] = useState(true) const [isSearchActive, setIsSearchActive] = useState(false) @@ -428,7 +422,6 @@ function SubBlockComponent({ const searchInputRef = useRef(null) const wandControlRef = useRef(null) - // Use webhook management hook when config has useWebhookUrl enabled const webhookManagement = useWebhookManagement({ blockId, triggerId: undefined, @@ -515,10 +508,12 @@ function SubBlockComponent({ | null | undefined + const contextValues = dependencyContext ?? (isPreview ? subBlockValues : undefined) + const { finalDisabled: gatedDisabled } = useDependsOnGate(blockId, config, { disabled, isPreview, - previewContextValues: isPreview ? subBlockValues : undefined, + previewContextValues: contextValues, }) const isDisabled = gatedDisabled @@ -802,7 +797,7 @@ function SubBlockComponent({ disabled={isDisabled} isPreview={isPreview} previewValue={previewValue} - previewContextValues={isPreview ? subBlockValues : undefined} + previewContextValues={contextValues} /> ) @@ -814,7 +809,7 @@ function SubBlockComponent({ disabled={isDisabled} isPreview={isPreview} previewValue={previewValue} - previewContextValues={isPreview ? subBlockValues : undefined} + previewContextValues={contextValues} /> ) @@ -826,7 +821,7 @@ function SubBlockComponent({ disabled={isDisabled} isPreview={isPreview} previewValue={previewValue} - previewContextValues={isPreview ? subBlockValues : undefined} + previewContextValues={contextValues} /> ) @@ -838,7 +833,7 @@ function SubBlockComponent({ disabled={isDisabled} isPreview={isPreview} previewValue={previewValue} - previewContextValues={isPreview ? subBlockValues : undefined} + previewContextValues={contextValues} /> ) @@ -850,7 +845,7 @@ function SubBlockComponent({ disabled={isDisabled} isPreview={isPreview} previewValue={previewValue} - previewContextValues={isPreview ? subBlockValues : undefined} + previewContextValues={contextValues} /> ) @@ -873,7 +868,7 @@ function SubBlockComponent({ disabled={isDisabled} isPreview={isPreview} previewValue={previewValue as any} - previewContextValues={isPreview ? subBlockValues : undefined} + previewContextValues={contextValues} /> ) @@ -885,7 +880,7 @@ function SubBlockComponent({ disabled={isDisabled} isPreview={isPreview} previewValue={previewValue as any} - previewContextValues={isPreview ? subBlockValues : undefined} + previewContextValues={contextValues} /> ) @@ -897,7 +892,7 @@ function SubBlockComponent({ disabled={isDisabled} isPreview={isPreview} previewValue={previewValue as any} - previewContextValues={isPreview ? subBlockValues : undefined} + previewContextValues={contextValues} /> ) @@ -922,7 +917,7 @@ function SubBlockComponent({ isPreview={isPreview} previewValue={previewValue as any} disabled={isDisabled} - previewContextValues={isPreview ? subBlockValues : undefined} + previewContextValues={contextValues} /> ) @@ -958,7 +953,7 @@ function SubBlockComponent({ disabled={isDisabled} isPreview={isPreview} previewValue={previewValue} - previewContextValues={isPreview ? subBlockValues : undefined} + previewContextValues={contextValues} /> ) @@ -992,7 +987,7 @@ function SubBlockComponent({ disabled={isDisabled} isPreview={isPreview} previewValue={previewValue as any} - previewContextValues={isPreview ? subBlockValues : undefined} + previewContextValues={contextValues} /> ) @@ -1004,7 +999,7 @@ function SubBlockComponent({ disabled={isDisabled} isPreview={isPreview} previewValue={previewValue} - previewContextValues={isPreview ? subBlockValues : undefined} + previewContextValues={contextValues} /> ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx index a7a5d7c38c..9f1905c830 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx @@ -571,7 +571,6 @@ export function Editor() { isPreview={false} subBlockValues={subBlockState} disabled={!canEditBlock} - fieldDiffStatus={undefined} allowExpandInPreview={false} canonicalToggle={ isCanonicalSwap && canonicalMode && canonicalId @@ -635,7 +634,6 @@ export function Editor() { isPreview={false} subBlockValues={subBlockState} disabled={!canEditBlock} - fieldDiffStatus={undefined} allowExpandInPreview={false} /> {index < advancedOnlySubBlocks.length - 1 && ( From a29afd2757c11661352afc0df99f0548cd1235c6 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 12 Feb 2026 10:41:07 -0800 Subject: [PATCH 04/17] cleanup --- .../components/tool-input/tool-input.tsx | 207 ++++++++---------- 1 file changed, 90 insertions(+), 117 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index b35d59e648..2cf1305bbf 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -340,6 +340,80 @@ const BUILT_IN_TOOL_TYPES = new Set([ 'workflow', ]) +/** + * Checks if a block supports multiple operations. + * + * @param blockType - The block type to check + * @returns `true` if the block has more than one tool operation available + */ +function hasMultipleOperations(blockType: string): boolean { + const block = getAllBlocks().find((b) => b.type === blockType) + return (block?.tools?.access?.length || 0) > 1 +} + +/** + * Gets the available operation options for a multi-operation tool. + * + * @param blockType - The block type to get operations for + * @returns Array of operation options with label and id properties + */ +function getOperationOptions(blockType: string): { label: string; id: string }[] { + const block = getAllBlocks().find((b) => b.type === blockType) + if (!block || !block.tools?.access) return [] + + const operationSubBlock = block.subBlocks.find((sb) => sb.id === 'operation') + if ( + operationSubBlock && + operationSubBlock.type === 'dropdown' && + Array.isArray(operationSubBlock.options) + ) { + return operationSubBlock.options as { label: string; id: string }[] + } + + return block.tools.access.map((toolId) => { + try { + const toolParams = getToolParametersConfig(toolId) + return { + id: toolId, + label: toolParams?.toolConfig?.name || toolId, + } + } catch (error) { + logger.error(`Error getting tool config for ${toolId}:`, error) + return { id: toolId, label: toolId } + } + }) +} + +/** + * Gets the correct tool ID for a given operation. + * + * @param blockType - The block type + * @param operation - The selected operation (for multi-operation tools) + * @returns The tool ID to use for execution, or `undefined` if not found + */ +function getToolIdForOperation(blockType: string, operation?: string): string | undefined { + const block = getAllBlocks().find((b) => b.type === blockType) + if (!block || !block.tools?.access) return undefined + + if (block.tools.access.length === 1) { + return block.tools.access[0] + } + + if (operation && block.tools?.config?.tool) { + try { + return block.tools.config.tool({ operation }) + } catch (error) { + logger.error('Error selecting tool for operation:', error) + } + } + + if (operation && block.tools.access.includes(operation)) { + return operation + } + + return block.tools.access[0] +} + /** * Creates a styled icon element for tool items in the selection dropdown. * @@ -605,105 +679,12 @@ export const ToolInput = memo(function ToolInput({ if (hasMultipleOperations(blockType)) { return false } - // Allow multiple instances for workflow and knowledge blocks - // Each instance can target a different workflow/knowledge base if (blockType === 'workflow' || blockType === 'knowledge') { return false } return selectedTools.some((tool) => tool.toolId === toolId) } - /** - * Checks if a block supports multiple operations. - * - * @param blockType - The block type to check - * @returns `true` if the block has more than one tool operation available - */ - const hasMultipleOperations = (blockType: string): boolean => { - const block = getAllBlocks().find((block) => block.type === blockType) - return (block?.tools?.access?.length || 0) > 1 - } - - /** - * Gets the available operation options for a multi-operation tool. - * - * @remarks - * First attempts to find options from the block's operation dropdown subBlock, - * then falls back to creating options from the tools.access array. - * - * @param blockType - The block type to get operations for - * @returns Array of operation options with label and id properties - */ - const getOperationOptions = (blockType: string): { label: string; id: string }[] => { - const block = getAllBlocks().find((block) => block.type === blockType) - if (!block || !block.tools?.access) return [] - - // Look for an operation dropdown in the block's subBlocks - const operationSubBlock = block.subBlocks.find((sb) => sb.id === 'operation') - if ( - operationSubBlock && - operationSubBlock.type === 'dropdown' && - Array.isArray(operationSubBlock.options) - ) { - return operationSubBlock.options as { label: string; id: string }[] - } - - // Fallback: create options from tools.access - return block.tools.access.map((toolId) => { - try { - const toolParams = getToolParametersConfig(toolId) - return { - id: toolId, - label: toolParams?.toolConfig?.name || toolId, - } - } catch (error) { - logger.error(`Error getting tool config for ${toolId}:`, error) - return { - id: toolId, - label: toolId, - } - } - }) - } - - /** - * Gets the correct tool ID for a given operation. - * - * @remarks - * For single-tool blocks, returns the first tool. For multi-operation blocks, - * uses the block's tool selection function or matches the operation to a tool ID. - * - * @param blockType - The block type - * @param operation - The selected operation (for multi-operation tools) - * @returns The tool ID to use for execution, or `undefined` if not found - */ - const getToolIdForOperation = (blockType: string, operation?: string): string | undefined => { - const block = getAllBlocks().find((block) => block.type === blockType) - if (!block || !block.tools?.access) return undefined - - // If there's only one tool, return it - if (block.tools.access.length === 1) { - return block.tools.access[0] - } - - // If there's an operation and a tool selection function, use it - if (operation && block.tools?.config?.tool) { - try { - return block.tools.config.tool({ operation }) - } catch (error) { - logger.error('Error selecting tool for operation:', error) - } - } - - // If there's an operation that matches a tool ID, use it - if (operation && block.tools.access.includes(operation)) { - return operation - } - - // Default to first tool - return block.tools.access[0] - } - const handleSelectTool = useCallback( (toolBlock: (typeof toolBlocks)[0]) => { if (isPreview || disabled) return @@ -746,17 +727,7 @@ export const ToolInput = memo(function ToolInput({ setOpen(false) }, - [ - isPreview, - disabled, - hasMultipleOperations, - getOperationOptions, - getToolIdForOperation, - isToolAlreadySelected, - blockId, - selectedTools, - setStoreValue, - ] + [isPreview, disabled, isToolAlreadySelected, selectedTools, setStoreValue] ) const handleAddCustomTool = useCallback( @@ -1013,19 +984,22 @@ export const ToolInput = memo(function ToolInput({ setDragOverIndex(null) } - const handleMcpToolSelect = (newTool: StoredTool, closePopover = true) => { - setStoreValue([ - ...selectedTools.map((tool) => ({ - ...tool, - isExpanded: false, - })), - newTool, - ]) + const handleMcpToolSelect = useCallback( + (newTool: StoredTool, closePopover = true) => { + setStoreValue([ + ...selectedTools.map((tool) => ({ + ...tool, + isExpanded: false, + })), + newTool, + ]) - if (closePopover) { - setOpen(false) - } - } + if (closePopover) { + setOpen(false) + } + }, + [selectedTools, setStoreValue] + ) const handleDrop = (e: React.DragEvent, dropIndex: number) => { if (isPreview || disabled || draggedIndex === null || draggedIndex === dropIndex) return @@ -1421,7 +1395,6 @@ export const ToolInput = memo(function ToolInput({ permissionConfig.disableCustomTools, permissionConfig.disableMcpTools, availableWorkflows, - getToolIdForOperation, isToolAlreadySelected, ]) From c43f502ffb39e84f4839d5dbfc4a93d3f12ce658 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 12 Feb 2026 11:23:14 -0800 Subject: [PATCH 05/17] fix(tool-input): render uncovered tool params alongside subblocks The SubBlock-first rendering path was hard-returning after rendering subblocks, so tool params without matching subblocks (like inputMapping for workflow tools) were never rendered. Now renders subblocks first, then any remaining displayParams not covered by subblocks via the legacy ParameterWithLabel fallback. Co-Authored-By: Claude Opus 4.6 --- .../components/tool-input/tool-input.tsx | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index 2cf1305bbf..9d166e6057 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -1771,9 +1771,15 @@ export const ToolInput = memo(function ToolInput({ {/* Tool parameters */} {(() => { + const renderedElements: React.ReactNode[] = [] + // SubBlock-first rendering for registry tools if (useSubBlocks && displaySubBlocks.length > 0) { - return displaySubBlocks.map((sb) => { + const coveredParamIds = new Set( + displaySubBlocks.map((sb) => sb.canonicalParamId || sb.id) + ) + + displaySubBlocks.forEach((sb) => { const effectiveParamId = sb.canonicalParamId || sb.id // Compute canonical toggle for basic/advanced mode switching @@ -1808,7 +1814,7 @@ export const ToolInput = memo(function ToolInput({ ? sb : { ...sb, title: formatParameterLabel(effectiveParamId) } - return ( + renderedElements.push( ) }) + + // Render remaining tool params not covered by subblocks + // (e.g. inputMapping for workflow tools with custom UI) + const uncoveredParams = displayParams.filter( + (param) => + !coveredParamIds.has(param.id) && evaluateParameterCondition(param, tool) + ) + + uncoveredParams.forEach((param) => { + renderedElements.push( + + {(wandControlRef: React.MutableRefObject) => + renderParameterInput( + param, + tool.params?.[param.id] || '', + (value) => handleParamChange(toolIndex, param.id, value), + toolIndex, + toolContextValues as Record, + wandControlRef + ) + } + + ) + }) + + return renderedElements } // Fallback: legacy ToolParameterConfig-based rendering @@ -1831,8 +1872,6 @@ export const ToolInput = memo(function ToolInput({ evaluateParameterCondition(param, tool) ) - const renderedElements: React.ReactNode[] = [] - filteredParams.forEach((param) => { renderedElements.push( Date: Thu, 12 Feb 2026 11:27:50 -0800 Subject: [PATCH 06/17] fix(tool-input): auto-refresh workflow inputs after redeploy After redeploying a child workflow via the stale badge, the workflow state cache was not invalidated, so WorkflowInputMapperInput kept showing stale input fields until page refresh. Now invalidates workflowKeys.state on deploy success. Co-Authored-By: Claude Opus 4.6 --- apps/sim/hooks/queries/workflows.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/sim/hooks/queries/workflows.ts b/apps/sim/hooks/queries/workflows.ts index 19effa8bd4..5e50194c4e 100644 --- a/apps/sim/hooks/queries/workflows.ts +++ b/apps/sim/hooks/queries/workflows.ts @@ -642,6 +642,10 @@ export function useDeployChildWorkflow() { queryClient.invalidateQueries({ queryKey: workflowKeys.deploymentStatus(variables.workflowId), }) + // Invalidate workflow state so tool input mappings refresh + queryClient.invalidateQueries({ + queryKey: workflowKeys.state(variables.workflowId), + }) // Also invalidate deployment queries queryClient.invalidateQueries({ queryKey: deploymentKeys.info(variables.workflowId), From a25b26e1e9569ba94716ca733a65380eba31710b Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 12 Feb 2026 11:42:45 -0800 Subject: [PATCH 07/17] fix(tool-input): correct workflow selector visibility and tighten (optional) spacing - Set workflowId param to user-only in workflow_executor tool config so "Select Workflow" no longer shows "(optional)" indicator - Tighten (optional) label spacing with -ml-[3px] to counteract parent Label's gap-[6px], making it feel inline with the label text Co-Authored-By: Claude Opus 4.6 --- .../components/tool-input/components/tools/parameter.tsx | 4 +++- .../tool-input/components/tools/sub-block-renderer.tsx | 4 +++- apps/sim/tools/workflow/executor.ts | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx index fe9ed32740..f1b34bec39 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx @@ -91,7 +91,9 @@ export function ParameterWithLabel({ {title} {isRequired && visibility === 'user-only' && *} {visibility !== 'user-only' && ( - (optional) + + (optional) + )}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx index 177dd55a0e..64c68b0583 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx @@ -84,7 +84,9 @@ export function ToolSubBlockRenderer({ const labelSuffix = useMemo( () => isOptionalForUser ? ( - (optional) + + (optional) + ) : null, [isOptionalForUser] ) diff --git a/apps/sim/tools/workflow/executor.ts b/apps/sim/tools/workflow/executor.ts index 894908309d..44ebff2b5b 100644 --- a/apps/sim/tools/workflow/executor.ts +++ b/apps/sim/tools/workflow/executor.ts @@ -18,7 +18,7 @@ export const workflowExecutorTool: ToolConfig< workflowId: { type: 'string', required: true, - visibility: 'user-or-llm', + visibility: 'user-only', description: 'The ID of the workflow to execute', }, inputMapping: { From 3e17627d781fa02bed85b75c91168a6160a60330 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 12 Feb 2026 11:43:15 -0800 Subject: [PATCH 08/17] fix(tool-input): align (optional) text to baseline instead of center Use items-baseline instead of items-center on Label flex containers so the smaller (optional) text aligns with the label text baseline rather than sitting slightly below it. Co-Authored-By: Claude Opus 4.6 --- .../components/tool-input/components/tools/parameter.tsx | 2 +- .../panel/components/editor/components/sub-block/sub-block.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx index f1b34bec39..ff8f43bfeb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx @@ -87,7 +87,7 @@ export function ParameterWithLabel({ return (
-