diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployment.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployment.ts index 1f2a350d87..b6a5d585e3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployment.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployment.ts @@ -1,7 +1,10 @@ import { useCallback, useState } from 'react' import { createLogger } from '@sim/logger' +import { runPreDeployChecks } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-predeploy-checks' import { useNotificationStore } from '@/stores/notifications' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { mergeSubblockState } from '@/stores/workflows/utils' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' const logger = createLogger('useDeployment') @@ -35,6 +38,24 @@ export function useDeployment({ return { success: true, shouldOpenModal: true } } + const { blocks, edges, loops, parallels } = useWorkflowStore.getState() + const liveBlocks = mergeSubblockState(blocks, workflowId) + const checkResult = runPreDeployChecks({ + blocks: liveBlocks, + edges, + loops, + parallels, + workflowId, + }) + if (!checkResult.passed) { + addNotification({ + level: 'error', + message: checkResult.error || 'Pre-deploy validation failed', + workflowId, + }) + return { success: false, shouldOpenModal: false } + } + setIsDeploying(true) try { const response = await fetch(`/api/workflows/${workflowId}/deploy`, { 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 96% 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 index 0496489d41..255d859079 100644 --- 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 @@ -4,6 +4,7 @@ import { Button, Combobox } from '@/components/emcn/components' import { getCanonicalScopesForProvider, getProviderIdFromServiceId, + getServiceConfigByProviderId, OAUTH_PROVIDERS, type OAuthProvider, type OAuthService, @@ -26,6 +27,11 @@ const getProviderIcon = (providerName: OAuthProvider) => { } const getProviderName = (providerName: OAuthProvider) => { + const serviceConfig = getServiceConfigByProviderId(providerName) + if (serviceConfig) { + return serviceConfig.name + } + const { baseProvider } = parseProvider(providerName) const baseProviderConfig = OAUTH_PROVIDERS[baseProvider] @@ -54,7 +60,7 @@ export function ToolCredentialSelector({ onChange, provider, requiredScopes = [], - label = 'Select account', + label, serviceId, disabled = false, }: ToolCredentialSelectorProps) { @@ -64,6 +70,7 @@ export function ToolCredentialSelector({ const { activeWorkflowId } = useWorkflowRegistry() const selectedId = value || '' + const effectiveLabel = label || `Select ${getProviderName(provider)} account` const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId]) @@ -203,7 +210,7 @@ export function ToolCredentialSelector({ selectedValue={selectedId} onChange={handleComboboxChange} onOpenChange={handleOpenChange} - placeholder={label} + placeholder={effectiveLabel} disabled={disabled} editable={true} filterOptions={!isForeign} 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..d69ad776b1 --- /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,186 @@ +'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..0f9319ace3 --- /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, 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 + } +} + +/** + * SubBlock types whose store values are objects/arrays/non-strings. + * tool.params stores strings (via JSON.stringify), so when syncing + * back to the store we parse them to restore the native shape. + */ +const OBJECT_SUBBLOCK_TYPES = new Set(['file-upload', 'table', 'grouped-checkbox-list']) + +/** + * Bridges the subblock store with StoredTool.params via a synthetic store key, + * then delegates all rendering to SubBlock for full parity. + */ +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] ?? '' + const isObjectType = OBJECT_SUBBLOCK_TYPES.has(subBlock.type) + + const lastPushedToStoreRef = useRef(null) + const lastPushedToParamsRef = useRef(null) + + useEffect(() => { + if (!toolParamValue && lastPushedToStoreRef.current === null) { + lastPushedToStoreRef.current = toolParamValue + lastPushedToParamsRef.current = toolParamValue + return + } + if (toolParamValue !== lastPushedToStoreRef.current) { + lastPushedToStoreRef.current = toolParamValue + lastPushedToParamsRef.current = toolParamValue + + if (isObjectType && typeof toolParamValue === 'string' && toolParamValue) { + try { + const parsed = JSON.parse(toolParamValue) + if (typeof parsed === 'object' && parsed !== null) { + setStoreValue(parsed) + return + } + } catch { + // Not valid JSON — fall through to set as string + } + } + setStoreValue(toolParamValue) + } + }, [toolParamValue, setStoreValue, isObjectType]) + + 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]) + + const visibility = subBlock.paramVisibility ?? 'user-or-llm' + const isOptionalForUser = visibility !== 'user-only' + + 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 ff08547ec9..f92b8150af 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 @@ -23,6 +23,7 @@ import { isToolUnavailable, getMcpToolIssue as validateMcpTool, } from '@/lib/mcp/tool-validation' +import type { McpToolSchema } from '@/lib/mcp/types' import { getCanonicalScopesForProvider, getProviderIdFromServiceId, @@ -32,31 +33,26 @@ 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 { 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' +import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types' import { useMcpTools } from '@/hooks/mcp/use-mcp-tools' import { type CustomTool as CustomToolDefinition, @@ -74,26 +70,212 @@ 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') +/** + * Extracts canonical mode overrides scoped to a specific tool type. + * Canonical modes are stored with `{blockType}:{canonicalId}` keys to prevent + * cross-tool collisions when multiple tools share the same canonicalParamId. + */ +function scopeCanonicalOverrides( + overrides: CanonicalModeOverrides | undefined, + blockType: string | undefined +): CanonicalModeOverrides | undefined { + if (!overrides || !blockType) return undefined + const prefix = `${blockType}:` + let scoped: CanonicalModeOverrides | undefined + for (const [key, val] of Object.entries(overrides)) { + if (key.startsWith(prefix) && val) { + if (!scoped) scoped = {} + scoped[key.slice(prefix.length)] = val + } + } + return scoped +} + +/** + * 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, + onChange, + disabled, + workflowId, +}: { + blockId: string + paramId: string + value: string + onChange: (value: string) => void + disabled: boolean + workflowId: string +}) { + const { data: workflowState, isLoading } = useWorkflowState(workflowId) + const inputFields = useMemo( + () => (workflowState?.blocks ? extractInputFieldsFromBlocks(workflowState.blocks) : []), + [workflowState?.blocks] + ) + + const parsedValue = useMemo(() => { + try { + return value ? JSON.parse(value) : {} + } catch { + return {} + } + }, [value]) + + const handleFieldChange = useCallback( + (fieldName: string, fieldValue: string) => { + const newValue = { ...parsedValue, [fieldName]: fieldValue } + onChange(JSON.stringify(newValue)) + }, + [parsedValue, onChange] + ) + + if (!workflowId) { + return ( +
+ Select a workflow to configure its inputs +
+ ) + } + + if (isLoading) { + return ( +
+ +
+ ) + } + + if (inputFields.length === 0) { + return ( +
+ This workflow has no custom input fields +
+ ) + } + + return ( +
+ {inputFields.map((field: { name: string; type: string }) => ( + handleFieldChange(field.name, newValue)} + disabled={disabled} + config={{ + id: `${paramId}-${field.name}`, + type: 'short-input', + title: field.name, + }} + /> + ))} +
+ ) +} + +/** + * Badge component showing deployment status for workflow tools + */ +function WorkflowToolDeployBadge({ + workflowId, + onDeploySuccess, +}: { + workflowId: string + onDeploySuccess?: () => void +}) { + const { data, isLoading } = useChildDeploymentStatus(workflowId) + const deployMutation = useDeployChildWorkflow() + const userPermissions = useUserPermissionsContext() + + const isDeployed = data?.isDeployed ?? null + const needsRedeploy = data?.needsRedeploy ?? false + const isDeploying = deployMutation.isPending + + const deployWorkflow = useCallback(() => { + if (isDeploying || !workflowId || !userPermissions.canAdmin) return + + deployMutation.mutate( + { workflowId }, + { + onSuccess: () => { + onDeploySuccess?.() + }, + } + ) + }, [isDeploying, workflowId, userPermissions.canAdmin, deployMutation, onDeploySuccess]) + + if (isLoading || (isDeployed && !needsRedeploy)) { + return null + } + + if (typeof isDeployed !== 'boolean') { + return null + } + + return ( + + + { + e.stopPropagation() + e.preventDefault() + if (!isDeploying && userPermissions.canAdmin) { + deployWorkflow() + } + }} + > + {isDeploying ? 'Deploying...' : !isDeployed ? 'undeployed' : 'redeploy'} + + + + + {!userPermissions.canAdmin + ? 'Admin permission required to deploy' + : !isDeployed + ? 'Click to deploy' + : 'Click to redeploy'} + + + + ) +} + /** * Props for the ToolInput component */ @@ -112,37 +294,6 @@ interface ToolInputProps { 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. * @@ -186,801 +337,122 @@ function resolveCustomToolFromReference( } /** - * Generic sync wrapper that synchronizes store values with local component state. + * Set of built-in tool types that are core platform tools. * * @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. + * These are distinguished from third-party integrations for categorization + * in the tool selection dropdown. + */ +const BUILT_IN_TOOL_TYPES = new Set([ + 'api', + 'file', + 'function', + 'knowledge', + 'search', + 'thinking', + 'image_generator', + 'video_generator', + 'vision', + 'translate', + 'tts', + 'stt', + 'memory', + 'webhook_request', + 'workflow', +]) + +/** + * Checks if a block supports multiple operations. * - * @typeParam T - The type of the store value being synchronized + * @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 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 + * @param blockType - The block type to get operations for + * @returns Array of operation options with label and id properties */ -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) +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 }[] + } - useEffect(() => { - if (storeValue != null) { - const transformedValue = transformer ? transformer(storeValue) : String(storeValue) - if (transformedValue !== value) { - onChange(transformedValue) + 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) } - }, [storeValue, value, onChange, transformer]) + } + + if (operation && block.tools.access.includes(operation)) { + return operation + } - return <>{children} + return block.tools.access[0] } -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 -}) { +/** + * Creates a styled icon element for tool items in the selection dropdown. + * + * @param bgColor - Background color for the icon container + * @param IconComponent - The Lucide icon component to render + * @returns A styled div containing the icon with consistent dimensions + */ +function createToolIcon( + bgColor: string, + IconComponent: React.ComponentType<{ className?: string }> +) { 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 - ) - - const options = availableWorkflows.map((workflow) => ({ - label: workflow.name, - value: workflow.id, - })) - - return ( - - - - ) -} - -function WorkflowInputMapperSyncWrapper({ - blockId, - paramId, - value, - onChange, - disabled, - workflowId, -}: { - blockId: string - paramId: string - value: string - onChange: (value: string) => void - disabled: boolean - workflowId: string -}) { - const { data: workflowState, isLoading } = useWorkflowState(workflowId) - const inputFields = useMemo( - () => (workflowState?.blocks ? extractInputFieldsFromBlocks(workflowState.blocks) : []), - [workflowState?.blocks] - ) - - const parsedValue = useMemo(() => { - try { - return value ? JSON.parse(value) : {} - } catch { - return {} - } - }, [value]) - - const handleFieldChange = useCallback( - (fieldName: string, fieldValue: any) => { - const newValue = { ...parsedValue, [fieldName]: fieldValue } - onChange(JSON.stringify(newValue)) - }, - [parsedValue, onChange] - ) - - if (!workflowId) { - return ( -
- Select a workflow to configure its inputs -
- ) - } - - if (isLoading) { - return ( -
- -
- ) - } - - if (inputFields.length === 0) { - return ( -
- This workflow has no custom input fields -
- ) - } - - return ( -
- {inputFields.map((field: any) => ( - handleFieldChange(field.name, newValue)} - disabled={disabled} - config={{ - id: `${paramId}-${field.name}`, - type: 'short-input', - title: field.name, - }} - /> - ))} -
- ) -} - -function CodeEditorSyncWrapper({ - blockId, - paramId, - value, - onChange, - disabled, - uiComponent, - currentToolParams, -}: { - blockId: string - paramId: string - value: string - onChange: (value: 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 - */ -function WorkflowToolDeployBadge({ - workflowId, - onDeploySuccess, -}: { - workflowId: string - onDeploySuccess?: () => void -}) { - const { data, isLoading } = useChildDeploymentStatus(workflowId) - const deployMutation = useDeployChildWorkflow() - const userPermissions = useUserPermissionsContext() - - const isDeployed = data?.isDeployed ?? null - const needsRedeploy = data?.needsRedeploy ?? false - const isDeploying = deployMutation.isPending - - const deployWorkflow = useCallback(() => { - if (isDeploying || !workflowId || !userPermissions.canAdmin) return - - deployMutation.mutate( - { workflowId }, - { - onSuccess: () => { - onDeploySuccess?.() - }, - } - ) - }, [isDeploying, workflowId, userPermissions.canAdmin, deployMutation, onDeploySuccess]) - - if (isLoading || (isDeployed && !needsRedeploy)) { - return null - } - - if (typeof isDeployed !== 'boolean') { - return null - } - - return ( - - - { - e.stopPropagation() - e.preventDefault() - if (!isDeploying && userPermissions.canAdmin) { - deployWorkflow() - } - }} - > - {isDeploying ? 'Deploying...' : !isDeployed ? 'undeployed' : 'redeploy'} - - - - - {!userPermissions.canAdmin - ? 'Admin permission required to deploy' - : !isDeployed - ? 'Click to deploy' - : 'Click to redeploy'} - - - - ) -} - -/** - * Set of built-in tool types that are core platform tools. - * - * @remarks - * These are distinguished from third-party integrations for categorization - * in the tool selection dropdown. - */ -const BUILT_IN_TOOL_TYPES = new Set([ - 'api', - 'file', - 'function', - 'knowledge', - 'search', - 'thinking', - 'image_generator', - 'video_generator', - 'vision', - 'translate', - 'tts', - 'stt', - 'memory', - 'webhook_request', - 'workflow', -]) - -/** - * Creates a styled icon element for tool items in the selection dropdown. - * - * @param bgColor - Background color for the icon container - * @param IconComponent - The Lucide icon component to render - * @returns A styled div containing the icon with consistent dimensions - */ -function createToolIcon(bgColor: string, IconComponent: any) { - return ( -
- -
+
+ +
) } @@ -1014,6 +486,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[] = @@ -1030,12 +510,7 @@ export const ToolInput = memo(function ToolInput({ const shouldFetchCustomTools = !isPreview || hasReferenceOnlyCustomTools const { data: customTools = [] } = useCustomTools(shouldFetchCustomTools ? workspaceId : '') - const { - mcpTools, - isLoading: mcpLoading, - error: mcpError, - refreshTools, - } = useMcpTools(workspaceId) + const { mcpTools, isLoading: mcpLoading } = useMcpTools(workspaceId) const { data: mcpServers = [], isLoading: mcpServersLoading } = useMcpServers(workspaceId) const { data: storedMcpTools = [] } = useStoredMcpTools(workspaceId) @@ -1044,7 +519,6 @@ export const ToolInput = memo(function ToolInput({ const openSettingsModal = useSettingsModalStore((state) => state.openModal) const mcpDataLoading = mcpLoading || mcpServersLoading - // Fetch workflows for the Workflows section in the dropdown const { data: workflowsList = [] } = useWorkflows(workspaceId, { syncRegistry: false }) const availableWorkflows = useMemo( () => workflowsList.filter((w) => w.id !== workflowId), @@ -1082,7 +556,7 @@ export const ToolInput = memo(function ToolInput({ ) || storedMcpTools.find((st) => 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( { @@ -1225,159 +699,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 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. - * - * @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] - } - - /** - * 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 +721,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]) { @@ -1420,18 +747,7 @@ export const ToolInput = memo(function ToolInput({ setOpen(false) }, - [ - isPreview, - disabled, - hasMultipleOperations, - getOperationOptions, - getToolIdForOperation, - isToolAlreadySelected, - initializeToolParams, - blockId, - selectedTools, - setStoreValue, - ] + [isPreview, disabled, isToolAlreadySelected, selectedTools, setStoreValue] ) const handleAddCustomTool = useCallback( @@ -1541,7 +857,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 @@ -1597,10 +913,6 @@ export const ToolInput = memo(function ToolInput({ return } - const initialParams = initializeToolParams(newToolId, toolParams.userInputParameters, blockId) - - 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 = {} @@ -1626,21 +938,13 @@ export const ToolInput = memo(function ToolInput({ ...tool, toolId: newToolId, operation, - params: { ...initialParams, ...preservedParams }, // Preserve all compatible existing values + params: preservedParams, } : tool ) ) }, - [ - isPreview, - disabled, - selectedTools, - getToolIdForOperation, - initializeToolParams, - blockId, - setStoreValue, - ] + [isPreview, disabled, selectedTools, getToolIdForOperation, blockId, setStoreValue] ) const handleUsageControlChange = useCallback( @@ -1700,265 +1004,58 @@ 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 e.preventDefault() - const newTools = [...selectedTools] - const draggedTool = newTools[draggedIndex] - - newTools.splice(draggedIndex, 1) - - if (dropIndex === selectedTools.length) { - newTools.push(draggedTool) - } else { - const adjustedDropIndex = draggedIndex < dropIndex ? dropIndex - 1 : dropIndex - newTools.splice(adjustedDropIndex, 0, draggedTool) - } - - setStoreValue(newTools) - setDraggedIndex(null) - setDragOverIndex(null) - } - - const IconComponent = ({ icon: Icon, className }: { icon: any; className?: string }) => { - if (!Icon) return null - 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) - window.dispatchEvent(new CustomEvent('open-settings', { detail: { tab: 'mcp' } })) - }, - disabled: isPreview, - }) - } - if (actionItems.length > 0) { - groups.push({ items: actionItems }) - } - - // Custom Tools section - if (!permissionConfig.disableCustomTools && customTools.length > 0) { - groups.push({ - section: 'Custom Tools', - items: customTools.map((customTool) => { - const alreadySelected = isCustomToolAlreadySelected(customTool.id) - return { - label: customTool.title, - value: `custom-${customTool.id}`, - iconElement: createToolIcon('#3B82F6', WrenchIcon), - disabled: isPreview || alreadySelected, - onSelect: () => { - if (alreadySelected) return - const newTool: StoredTool = { - type: 'custom-tool', - customToolId: customTool.id, - usageControl: 'auto', - isExpanded: true, - } - setStoreValue([ - ...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), - newTool, - ]) - setOpen(false) - }, - } - }), - }) - } - - // MCP Tools section - if (!permissionConfig.disableMcpTools && availableMcpTools.length > 0) { - groups.push({ - section: 'MCP Tools', - items: availableMcpTools.map((mcpTool) => { - const server = mcpServers.find((s) => s.id === mcpTool.serverId) - const alreadySelected = isMcpToolAlreadySelected(mcpTool.id) - return { - label: mcpTool.name, - value: `mcp-${mcpTool.id}`, - iconElement: createToolIcon(mcpTool.bgColor || '#6366F1', mcpTool.icon || McpIcon), - onSelect: () => { - if (alreadySelected) return - const newTool: StoredTool = { - type: 'mcp', - title: mcpTool.name, - toolId: mcpTool.id, - params: { - serverId: mcpTool.serverId, - ...(server?.url && { serverUrl: server.url }), - toolName: mcpTool.name, - serverName: mcpTool.serverName, - }, - isExpanded: true, - usageControl: 'auto', - schema: { - ...mcpTool.inputSchema, - description: mcpTool.description, - }, - } - handleMcpToolSelect(newTool, true) - }, - disabled: isPreview || disabled || alreadySelected, - } - }), - }) - } - - // Split tool blocks into built-in tools and integrations - const builtInTools = toolBlocks.filter((block) => BUILT_IN_TOOL_TYPES.has(block.type)) - const integrations = toolBlocks.filter((block) => !BUILT_IN_TOOL_TYPES.has(block.type)) - - // Built-in Tools section - if (builtInTools.length > 0) { - groups.push({ - section: 'Built-in Tools', - items: builtInTools.map((block) => { - const toolId = getToolIdForOperation(block.type, undefined) - const alreadySelected = toolId ? isToolAlreadySelected(toolId, block.type) : false - return { - label: block.name, - value: `builtin-${block.type}`, - iconElement: createToolIcon(block.bgColor, block.icon), - disabled: isPreview || alreadySelected, - onSelect: () => handleSelectTool(block), - } - }), - }) - } + const newTools = [...selectedTools] + const draggedTool = newTools[draggedIndex] - // Integrations section - if (integrations.length > 0) { - groups.push({ - section: 'Integrations', - items: integrations.map((block) => { - const toolId = getToolIdForOperation(block.type, undefined) - const alreadySelected = toolId ? isToolAlreadySelected(toolId, block.type) : false - return { - label: block.name, - value: `builtin-${block.type}`, - iconElement: createToolIcon(block.bgColor, block.icon), - disabled: isPreview || alreadySelected, - onSelect: () => handleSelectTool(block), - } - }), - }) - } + newTools.splice(draggedIndex, 1) - // Workflows section - shows available workflows that can be executed as tools - if (availableWorkflows.length > 0) { - groups.push({ - section: 'Workflows', - items: availableWorkflows.map((workflow) => { - const alreadySelected = isWorkflowAlreadySelected(workflow.id) - return { - label: workflow.name, - value: `workflow-${workflow.id}`, - iconElement: createToolIcon('#6366F1', WorkflowIcon), - onSelect: () => { - if (alreadySelected) return - const newTool: StoredTool = { - type: 'workflow_input', - title: 'Workflow', - toolId: 'workflow_executor', - params: { - workflowId: workflow.id, - }, - isExpanded: true, - usageControl: 'auto', - } - setStoreValue([ - ...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), - newTool, - ]) - setOpen(false) - }, - disabled: isPreview || disabled || alreadySelected, - } - }), - }) + if (dropIndex === selectedTools.length) { + newTools.push(draggedTool) + } else { + const adjustedDropIndex = draggedIndex < dropIndex ? dropIndex - 1 : dropIndex + newTools.splice(adjustedDropIndex, 0, draggedTool) } - return groups - }, [ - customTools, - availableMcpTools, - mcpServers, - toolBlocks, - isPreview, - disabled, - selectedTools, - setStoreValue, - handleMcpToolSelect, - handleSelectTool, - permissionConfig.disableCustomTools, - permissionConfig.disableMcpTools, - availableWorkflows, - getToolIdForOperation, - isToolAlreadySelected, - isMcpToolAlreadySelected, - isCustomToolAlreadySelected, - isWorkflowAlreadySelected, - ]) - - const toolRequiresOAuth = (toolId: string): boolean => { - const toolParams = getToolParametersConfig(toolId) - return toolParams?.toolConfig?.oauth?.required || false + setStoreValue(newTools) + setDraggedIndex(null) + setDragOverIndex(null) } - const getToolOAuthConfig = (toolId: string) => { - const toolParams = getToolParametersConfig(toolId) - return toolParams?.toolConfig?.oauth + const IconComponent = ({ + icon: Icon, + className, + }: { + icon?: React.ComponentType<{ className?: string }> + className?: string + }) => { + if (!Icon) return null + return } - const evaluateParameterCondition = (param: any, tool: StoredTool): boolean => { + const evaluateParameterCondition = (param: ToolParameterConfig, tool: StoredTool): boolean => { if (!('uiComponent' in param) || !param.uiComponent?.condition) return true - const currentValues: Record = { operation: tool.operation, ...tool.params } + const currentValues: Record = { operation: tool.operation, ...tool.params } return evaluateSubBlockCondition( param.uiComponent.condition as SubBlockCondition, currentValues @@ -1966,26 +1063,18 @@ export const ToolInput = memo(function ToolInput({ } /** - * Renders the appropriate UI component for a tool parameter. + * Renders a parameter input for custom tools, MCP tools, and legacy registry + * tools that don't have SubBlockConfig definitions. * - * @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. - * - * @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 @@ -1998,7 +1087,7 @@ export const ToolInput = memo(function ToolInput({ ) } @@ -2016,11 +1107,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 +1139,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} /> ) @@ -2063,275 +1157,43 @@ export const ToolInput = memo(function ToolInput({ password={uiComponent.password || isPasswordParameter(param.id)} config={{ id: uniqueSubBlockId, - type: 'short-input', - title: param.id, - }} - value={value} - onChange={onChange} - disabled={disabled} - /> - ) - - case 'channel-selector': - return ( - - ) - - case 'user-selector': - return ( - - ) - - case 'project-selector': - return ( - - ) - - case 'oauth-input': - return ( - - ) - - 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 'code': - return ( - - ) - - case 'knowledge-base-selector': - return ( - - ) - - case 'document-selector': - return ( - ) - case 'document-tag-entry': + case 'oauth-input': return ( - ) - case 'knowledge-tag-filters': + case 'workflow-input-mapper': { + const selectedWorkflowId = currentToolParams?.workflowId || '' return ( - ) + } default: return ( @@ -2347,14 +1209,221 @@ export const ToolInput = memo(function ToolInput({ }} value={value} onChange={onChange} + wandControlRef={wandControlRef} + hideInternalWand={true} /> ) } } + /** + * 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[] = [] + + 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) + window.dispatchEvent(new CustomEvent('open-settings', { detail: { tab: 'mcp' } })) + }, + disabled: isPreview, + }) + } + if (actionItems.length > 0) { + groups.push({ items: actionItems }) + } + + if (!permissionConfig.disableCustomTools && customTools.length > 0) { + groups.push({ + section: 'Custom Tools', + items: customTools.map((customTool) => { + const alreadySelected = isCustomToolAlreadySelected(selectedTools, customTool.id) + return { + label: customTool.title, + value: `custom-${customTool.id}`, + iconElement: createToolIcon('#3B82F6', WrenchIcon), + disabled: isPreview || alreadySelected, + onSelect: () => { + if (alreadySelected) return + const newTool: StoredTool = { + type: 'custom-tool', + customToolId: customTool.id, + usageControl: 'auto', + isExpanded: true, + } + setStoreValue([ + ...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), + newTool, + ]) + setOpen(false) + }, + } + }), + }) + } + + if (!permissionConfig.disableMcpTools && availableMcpTools.length > 0) { + groups.push({ + section: 'MCP Tools', + items: availableMcpTools.map((mcpTool) => { + const server = mcpServers.find((s) => s.id === mcpTool.serverId) + const alreadySelected = isMcpToolAlreadySelected(selectedTools, mcpTool.id) + return { + label: mcpTool.name, + value: `mcp-${mcpTool.id}`, + iconElement: createToolIcon(mcpTool.bgColor || '#6366F1', mcpTool.icon || McpIcon), + onSelect: () => { + if (alreadySelected) return + const newTool: StoredTool = { + type: 'mcp', + title: mcpTool.name, + toolId: mcpTool.id, + params: { + serverId: mcpTool.serverId, + ...(server?.url && { serverUrl: server.url }), + toolName: mcpTool.name, + serverName: mcpTool.serverName, + }, + isExpanded: true, + usageControl: 'auto', + schema: { + ...mcpTool.inputSchema, + description: mcpTool.description, + }, + } + handleMcpToolSelect(newTool, true) + }, + disabled: isPreview || disabled || alreadySelected, + } + }), + }) + } + + const builtInTools = toolBlocks.filter((block) => BUILT_IN_TOOL_TYPES.has(block.type)) + const integrations = toolBlocks.filter((block) => !BUILT_IN_TOOL_TYPES.has(block.type)) + + if (builtInTools.length > 0) { + groups.push({ + section: 'Built-in Tools', + items: builtInTools.map((block) => { + const toolId = getToolIdForOperation(block.type, undefined) + const alreadySelected = toolId ? isToolAlreadySelected(toolId, block.type) : false + return { + label: block.name, + value: `builtin-${block.type}`, + iconElement: createToolIcon(block.bgColor, block.icon), + disabled: isPreview || alreadySelected, + onSelect: () => handleSelectTool(block), + } + }), + }) + } + + if (integrations.length > 0) { + groups.push({ + section: 'Integrations', + items: integrations.map((block) => { + const toolId = getToolIdForOperation(block.type, undefined) + const alreadySelected = toolId ? isToolAlreadySelected(toolId, block.type) : false + return { + label: block.name, + value: `builtin-${block.type}`, + iconElement: createToolIcon(block.bgColor, block.icon), + disabled: isPreview || alreadySelected, + onSelect: () => handleSelectTool(block), + } + }), + }) + } + + // Workflows section - shows available workflows that can be executed as tools + if (availableWorkflows.length > 0) { + groups.push({ + section: 'Workflows', + items: availableWorkflows.map((workflow) => { + const alreadySelected = isWorkflowAlreadySelected(selectedTools, workflow.id) + return { + label: workflow.name, + value: `workflow-${workflow.id}`, + iconElement: createToolIcon('#6366F1', WorkflowIcon), + onSelect: () => { + if (alreadySelected) return + const newTool: StoredTool = { + type: 'workflow_input', + title: 'Workflow', + toolId: 'workflow_executor', + params: { + workflowId: workflow.id, + }, + isExpanded: true, + usageControl: 'auto', + } + setStoreValue([ + ...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), + newTool, + ]) + setOpen(false) + }, + disabled: isPreview || disabled || alreadySelected, + } + }), + }) + } + + return groups + }, [ + customTools, + availableMcpTools, + mcpServers, + toolBlocks, + isPreview, + disabled, + selectedTools, + setStoreValue, + handleMcpToolSelect, + handleSelectTool, + permissionConfig.disableCustomTools, + permissionConfig.disableMcpTools, + availableWorkflows, + isToolAlreadySelected, + ]) + + const toolRequiresOAuth = (toolId: string): boolean => { + const toolParams = getToolParametersConfig(toolId) + return toolParams?.toolConfig?.oauth?.required || false + } + + const getToolOAuthConfig = (toolId: string) => { + const toolParams = getToolParametersConfig(toolId) + return toolParams?.toolConfig?.oauth + } + return (
- {/* Add Tool Combobox - always at top */} - {/* Selected Tools List */} {selectedTools.length > 0 && selectedTools.map((tool, toolIndex) => { - // Handle custom tools, MCP tools, and workflow tools differently const isCustomTool = tool.type === 'custom-tool' const isMcpTool = tool.type === 'mcp' const isWorkflowTool = tool.type === 'workflow' @@ -2379,13 +1446,11 @@ export const ToolInput = memo(function ToolInput({ ? toolBlocks.find((block) => block.type === tool.type) : null - // Get the current tool ID (may change based on operation) const currentToolId = !isCustomTool && !isMcpTool ? getToolIdForOperation(tool.type, tool.operation) || tool.toolId || '' : tool.toolId || '' - // Get tool parameters using the new utility with block type for UI components const toolParams = !isCustomTool && !isMcpTool && currentToolId ? getToolParametersConfig(currentToolId, tool.type, { @@ -2394,12 +1459,25 @@ export const ToolInput = memo(function ToolInput({ }) : null - // Build canonical index for proper dependency resolution + const toolScopedOverrides = scopeCanonicalOverrides(canonicalModeOverrides, tool.type) + + const subBlocksResult: SubBlocksForToolInput | null = + !isCustomTool && !isMcpTool && currentToolId + ? getSubBlocksForToolInput( + currentToolId, + tool.type, + { + operation: tool.operation, + ...tool.params, + }, + toolScopedOverrides + ) + : null + const toolCanonicalIndex: CanonicalIndex | null = toolBlock?.subBlocks ? buildCanonicalIndex(toolBlock.subBlocks) : null - // Build preview context with canonical resolution const toolContextValues = toolCanonicalIndex ? buildPreviewContextValues(tool.params || {}, { blockType: tool.type, @@ -2409,12 +1487,10 @@ export const ToolInput = memo(function ToolInput({ }) : tool.params || {} - // For custom tools, resolve from reference (new format) or use inline (legacy) const resolvedCustomTool = isCustomTool ? resolveCustomToolFromReference(tool, customTools) : null - // Derive title and schema from resolved tool or inline data const customToolTitle = isCustomTool ? tool.title || resolvedCustomTool?.title || 'Unknown Tool' : null @@ -2433,8 +1509,6 @@ export const ToolInput = memo(function ToolInput({ ) : [] - // For MCP tools, extract parameters from input schema - // Use cached schema from tool object if available, otherwise fetch from mcpTools const mcpTool = isMcpTool ? mcpTools.find((t) => t.id === tool.toolId) : null const mcpToolSchema = isMcpTool ? tool.schema || mcpTool?.inputSchema : null const mcpToolParams = @@ -2451,28 +1525,27 @@ export const ToolInput = memo(function ToolInput({ ) : [] - // Get all parameters to display - const displayParams = isCustomTool + 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 = !isCustomTool && !isMcpTool && currentToolId && toolRequiresOAuth(currentToolId) const oauthConfig = !isCustomTool && !isMcpTool && currentToolId ? getToolOAuthConfig(currentToolId) : null - // Determine if tool has expandable body content const hasOperations = !isCustomTool && !isMcpTool && hasMultipleOperations(tool.type) - const filteredDisplayParams = displayParams.filter((param) => - evaluateParameterCondition(param, tool) - ) - const hasToolBody = - hasOperations || (requiresOAuth && oauthConfig) || 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 ? isPreview ? (previewExpanded[toolIndex] ?? !!tool.isExpanded) @@ -2643,7 +1716,6 @@ export const ToolInput = memo(function ToolInput({ {!isCustomTool && isExpandedForDisplay && (
- {/* Operation dropdown for tools with multiple operations */} {(() => { const hasOperations = hasMultipleOperations(tool.type) const operationOptions = hasOperations ? getOperationOptions(tool.type) : [] @@ -2669,23 +1741,23 @@ export const ToolInput = memo(function ToolInput({ ) : null })()} - {/* OAuth credential selector if required */} {requiresOAuth && oauthConfig && (
- Account + Account *
handleParamChange(toolIndex, 'credential', value)} + onChange={(value: string) => + handleParamChange(toolIndex, 'credential', value) + } provider={oauthConfig.provider as OAuthProvider} requiredScopes={ toolBlock?.subBlocks?.find((sb) => sb.id === 'credential') ?.requiredScopes || getCanonicalScopesForProvider(oauthConfig.provider) } - label={`Select ${oauthConfig.provider} account`} serviceId={oauthConfig.provider} disabled={disabled} /> @@ -2693,119 +1765,141 @@ export const ToolInput = memo(function ToolInput({
)} - {/* Tool parameters */} {(() => { - 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' }), - {} - ) + if (useSubBlocks && displaySubBlocks.length > 0) { + const coveredParamIds = new Set( + displaySubBlocks.flatMap((sb) => { + const ids = [sb.id] + if (sb.canonicalParamId) ids.push(sb.canonicalParamId) + const cId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id] + if (cId) { + const group = toolCanonicalIndex?.groupsById[cId] + if (group) { + if (group.basicId) ids.push(group.basicId) + ids.push(...group.advancedIds) + } + } + return ids + }) ) - 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} - /> -
-
+ displaySubBlocks.forEach((sb) => { + const effectiveParamId = sb.id + 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 }, + toolScopedOverrides + ) + : undefined + + const canonicalToggleProp = + hasCanonicalPair && canonicalMode && canonicalId + ? { + mode: canonicalMode, + onToggle: () => { + const nextMode = + canonicalMode === 'advanced' ? 'basic' : 'advanced' + collaborativeSetBlockCanonicalMode( + blockId, + `${tool.type}:${canonicalId}`, + nextMode + ) + }, + } + : undefined + + const sbWithTitle = sb.title + ? sb + : { ...sb, title: formatParameterLabel(effectiveParamId) } + + renderedElements.push( + + ) + }) + + const uncoveredParams = displayParams.filter( + (param) => + !coveredParamIds.has(param.id) && evaluateParameterCondition(param, tool) ) - }) - // Render standalone parameters - standaloneParams.forEach((param) => { - renderedElements.push( -
-
- {param.uiComponent?.title || formatParameterLabel(param.id)} - {param.required && param.visibility === 'user-only' && ( - * - )} - {param.visibility === 'user-or-llm' && ( - - (optional) - - )} -
-
- {param.uiComponent ? ( + uncoveredParams.forEach((param) => { + renderedElements.push( + + {(wandControlRef: React.MutableRefObject) => 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)} - /> - )} -
-
+ } + + ) + }) + + return ( +
{renderedElements}
+ ) + } + + const filteredParams = displayParams.filter((param) => + evaluateParameterCondition(param, tool) + ) + + filteredParams.forEach((param) => { + renderedElements.push( + + {(wandControlRef: React.MutableRefObject) => + renderParameterInput( + param, + tool.params?.[param.id] || '', + (value) => handleParamChange(toolIndex, param.id, value), + toolIndex, + toolContextValues as Record, + wandControlRef + ) + } + ) }) @@ -2817,7 +1911,6 @@ export const ToolInput = memo(function ToolInput({ ) })} - {/* Custom Tool Modal */} { @@ -2831,11 +1924,9 @@ export const ToolInput = memo(function ToolInput({ editingToolIndex !== null && selectedTools[editingToolIndex]?.type === 'custom-tool' ? (() => { const storedTool = selectedTools[editingToolIndex] - // Resolve the full tool definition from reference or inline const resolved = resolveCustomToolFromReference(storedTool, customTools) if (resolved) { - // Find the database ID const dbTool = storedTool.customToolId ? customTools.find((t) => t.id === storedTool.customToolId) : customTools.find( @@ -2849,7 +1940,6 @@ export const ToolInput = memo(function ToolInput({ } } - // Fallback to inline definition (legacy format) return { id: customTools.find( (tool) => tool.schema?.function?.name === storedTool.schema?.function?.name 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..180b8bb12a 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,13 +68,15 @@ interface SubBlockProps { isPreview?: boolean subBlockValues?: Record disabled?: boolean - fieldDiffStatus?: FieldDiffStatus allowExpandInPreview?: boolean canonicalToggle?: { mode: 'basic' | 'advanced' disabled?: boolean onToggle?: () => void } + labelSuffix?: React.ReactNode + /** Provides sibling values for dependency resolution in non-preview contexts (e.g. tool-input) */ + dependencyContext?: Record } /** @@ -162,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 = ( @@ -202,7 +201,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 @@ -215,9 +215,10 @@ const renderLabel = ( return (
-
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 && ( 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/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index 7cba8deb7a..e12c964f82 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -62,9 +62,12 @@ export class AgentBlockHandler implements BlockHandler { await validateModelProvider(ctx.userId, model, ctx) const providerId = getProviderFromModel(model) - const formattedTools = await this.formatTools(ctx, filteredInputs.tools || []) + const formattedTools = await this.formatTools( + ctx, + filteredInputs.tools || [], + block.canonicalModes + ) - // Resolve skill metadata for progressive disclosure const skillInputs = filteredInputs.skills ?? [] let skillMetadata: Array<{ name: string; description: string }> = [] if (skillInputs.length > 0 && ctx.workspaceId) { @@ -221,7 +224,11 @@ export class AgentBlockHandler implements BlockHandler { }) } - private async formatTools(ctx: ExecutionContext, inputTools: ToolInput[]): Promise { + private async formatTools( + ctx: ExecutionContext, + inputTools: ToolInput[], + canonicalModes?: Record + ): Promise { if (!Array.isArray(inputTools)) return [] const filtered = inputTools.filter((tool) => { @@ -249,7 +256,7 @@ export class AgentBlockHandler implements BlockHandler { if (tool.type === 'custom-tool' && (tool.schema || tool.customToolId)) { return await this.createCustomTool(ctx, tool) } - return this.transformBlockTool(ctx, tool) + return this.transformBlockTool(ctx, tool, canonicalModes) } catch (error) { logger.error(`[AgentHandler] Error creating tool:`, { tool, error }) return null @@ -719,12 +726,17 @@ export class AgentBlockHandler implements BlockHandler { } } - private async transformBlockTool(ctx: ExecutionContext, tool: ToolInput) { + private async transformBlockTool( + ctx: ExecutionContext, + tool: ToolInput, + canonicalModes?: Record + ) { const transformedTool = await transformBlockTool(tool, { selectedOperation: tool.operation, getAllBlocks, getToolAsync: (toolId: string) => getToolAsync(toolId, ctx.workflowId), getTool, + canonicalModes, }) if (transformedTool) { 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), diff --git a/apps/sim/lib/workflows/comparison/normalize.test.ts b/apps/sim/lib/workflows/comparison/normalize.test.ts index 2cf9b925a4..9aa6c9b120 100644 --- a/apps/sim/lib/workflows/comparison/normalize.test.ts +++ b/apps/sim/lib/workflows/comparison/normalize.test.ts @@ -645,6 +645,18 @@ describe('Workflow Normalization Utilities', () => { const result = filterSubBlockIds(ids) expect(result).toEqual(['signingSecret']) }) + + it.concurrent('should exclude synthetic tool-input subBlock IDs', () => { + const ids = [ + 'toolConfig', + 'toolConfig-tool-0-query', + 'toolConfig-tool-0-url', + 'toolConfig-tool-1-status', + 'systemPrompt', + ] + const result = filterSubBlockIds(ids) + expect(result).toEqual(['systemPrompt', 'toolConfig']) + }) }) describe('normalizeTriggerConfigValues', () => { diff --git a/apps/sim/lib/workflows/comparison/normalize.ts b/apps/sim/lib/workflows/comparison/normalize.ts index 4a8ce18a28..70a584141d 100644 --- a/apps/sim/lib/workflows/comparison/normalize.ts +++ b/apps/sim/lib/workflows/comparison/normalize.ts @@ -411,7 +411,14 @@ export function extractBlockFieldsForComparison(block: BlockState): ExtractedBlo } /** - * Filters subBlock IDs to exclude system and trigger runtime subBlocks. + * Pattern matching synthetic subBlock IDs created by ToolSubBlockRenderer. + * These IDs follow the format `{subBlockId}-tool-{index}-{paramId}` and are + * mirrors of values already stored in toolConfig.value.tools[N].params. + */ +const SYNTHETIC_TOOL_SUBBLOCK_RE = /-tool-\d+-/ + +/** + * Filters subBlock IDs to exclude system, trigger runtime, and synthetic tool subBlocks. * * @param subBlockIds - Array of subBlock IDs to filter * @returns Filtered and sorted array of subBlock IDs @@ -422,6 +429,7 @@ export function filterSubBlockIds(subBlockIds: string[]): string[] { if (TRIGGER_RUNTIME_SUBBLOCK_IDS.includes(id)) return false if (SYSTEM_SUBBLOCK_IDS.some((sysId) => id === sysId || id.startsWith(`${sysId}_`))) return false + if (SYNTHETIC_TOOL_SUBBLOCK_RE.test(id)) return false return true }) .sort() diff --git a/apps/sim/providers/types.ts b/apps/sim/providers/types.ts index 1f1edfe949..cb75153c55 100644 --- a/apps/sim/providers/types.ts +++ b/apps/sim/providers/types.ts @@ -112,6 +112,8 @@ export interface ProviderToolConfig { required: string[] } usageControl?: ToolUsageControl + /** Block-level params transformer — converts SubBlock values to tool-ready params */ + paramsTransform?: (params: Record) => Record } export interface Message { diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index fed88f31c4..ee1b2bfc79 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -4,6 +4,12 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' import { env } from '@/lib/core/config/env' import { isHosted } from '@/lib/core/config/feature-flags' +import { + buildCanonicalIndex, + type CanonicalGroup, + getCanonicalValues, + isCanonicalPair, +} from '@/lib/workflows/subblocks/visibility' import { isCustomTool } from '@/executor/constants' import { getComputerUseModels, @@ -437,9 +443,10 @@ export async function transformBlockTool( getAllBlocks: () => any[] getTool: (toolId: string) => any getToolAsync?: (toolId: string) => Promise + canonicalModes?: Record } ): Promise { - const { selectedOperation, getAllBlocks, getTool, getToolAsync } = options + const { selectedOperation, getAllBlocks, getTool, getToolAsync, canonicalModes } = options const blockDef = getAllBlocks().find((b: any) => b.type === block.type) if (!blockDef) { @@ -516,12 +523,66 @@ export async function transformBlockTool( uniqueToolId = `${toolConfig.id}_${userProvidedParams.knowledgeBaseId}` } + const blockParamsFn = blockDef?.tools?.config?.params as + | ((p: Record) => Record) + | undefined + const blockInputDefs = blockDef?.inputs as Record | undefined + + const canonicalGroups: CanonicalGroup[] = blockDef?.subBlocks + ? Object.values(buildCanonicalIndex(blockDef.subBlocks).groupsById).filter(isCanonicalPair) + : [] + + const needsTransform = blockParamsFn || blockInputDefs || canonicalGroups.length > 0 + const paramsTransform = needsTransform + ? (params: Record): Record => { + let result = { ...params } + + for (const group of canonicalGroups) { + const { basicValue, advancedValue } = getCanonicalValues(group, result) + const scopedKey = `${block.type}:${group.canonicalId}` + const pairMode = canonicalModes?.[scopedKey] ?? 'basic' + const chosen = pairMode === 'advanced' ? advancedValue : basicValue + + const sourceIds = [group.basicId, ...group.advancedIds].filter(Boolean) as string[] + sourceIds.forEach((id) => delete result[id]) + + if (chosen !== undefined) { + result[group.canonicalId] = chosen + } + } + + if (blockParamsFn) { + const transformed = blockParamsFn(result) + result = { ...result, ...transformed } + } + + if (blockInputDefs) { + for (const [key, schema] of Object.entries(blockInputDefs)) { + const value = result[key] + if (typeof value === 'string' && value.trim().length > 0) { + const inputType = typeof schema === 'object' ? schema.type : schema + if (inputType === 'json' || inputType === 'array') { + try { + result[key] = JSON.parse(value.trim()) + } catch { + // Not valid JSON — keep as string + } + } + } + } + } + + return result + } + : undefined + return { id: uniqueToolId, name: toolName, description: toolDescription, params: userProvidedParams, parameters: llmSchema, + paramsTransform, } } @@ -1028,7 +1089,11 @@ export function getMaxOutputTokensForModel(model: string): number { * Prepare tool execution parameters, separating tool parameters from system parameters */ export function prepareToolExecution( - tool: { params?: Record; parameters?: Record }, + tool: { + params?: Record + parameters?: Record + paramsTransform?: (params: Record) => Record + }, llmArgs: Record, request: { workflowId?: string @@ -1045,8 +1110,15 @@ export function prepareToolExecution( toolParams: Record executionParams: Record } { - // Use centralized merge logic from tools/params - const toolParams = mergeToolParameters(tool.params || {}, llmArgs) as Record + let toolParams = mergeToolParameters(tool.params || {}, llmArgs) as Record + + if (tool.paramsTransform) { + try { + toolParams = tool.paramsTransform(toolParams) + } catch (err) { + logger.warn('paramsTransform failed, using raw params', { error: err }) + } + } const executionParams = { ...toolParams, diff --git a/apps/sim/serializer/index.ts b/apps/sim/serializer/index.ts index 622667d9fc..35b675d224 100644 --- a/apps/sim/serializer/index.ts +++ b/apps/sim/serializer/index.ts @@ -280,7 +280,7 @@ export class Serializer { }) } - return { + const serialized: SerializedBlock = { id: block.id, position: block.position, config: { @@ -300,6 +300,12 @@ export class Serializer { }, enabled: block.enabled, } + + if (block.data?.canonicalModes) { + serialized.canonicalModes = block.data.canonicalModes as Record + } + + return serialized } private extractParams(block: BlockState): Record { diff --git a/apps/sim/serializer/types.ts b/apps/sim/serializer/types.ts index 4f89bfb71c..8192014a4a 100644 --- a/apps/sim/serializer/types.ts +++ b/apps/sim/serializer/types.ts @@ -38,6 +38,8 @@ export interface SerializedBlock { color?: string } enabled: boolean + /** Canonical mode overrides from block.data (used by agent handler for tool param resolution) */ + canonicalModes?: Record } export interface SerializedLoop { diff --git a/apps/sim/tools/file/parser.ts b/apps/sim/tools/file/parser.ts index bcd8826d2b..e7740bfa5d 100644 --- a/apps/sim/tools/file/parser.ts +++ b/apps/sim/tools/file/parser.ts @@ -95,7 +95,7 @@ export const fileParserTool: ToolConfig = { filePath: { type: 'string', required: false, - visibility: 'user-only', + visibility: 'hidden', description: 'Path to the file(s). Can be a single path, URL, or an array of paths.', }, file: { diff --git a/apps/sim/tools/jira/add_attachment.ts b/apps/sim/tools/jira/add_attachment.ts index 260bcc0294..bd890e509e 100644 --- a/apps/sim/tools/jira/add_attachment.ts +++ b/apps/sim/tools/jira/add_attachment.ts @@ -36,7 +36,7 @@ export const jiraAddAttachmentTool: ToolConfig, + 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 map of tool param IDs to their resolved visibility + 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 (!(sb.id in toolParamVisibility)) { + visibility = 'hidden' + } else { + visibility = toolParamVisibility[sb.id] || 'user-or-llm' + } + } else if (effectiveParamId in toolParamVisibility) { + // Fallback: infer from tool param visibility + visibility = toolParamVisibility[effectiveParamId] + } 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 + } + } + + // 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, paramVisibility: visibility }) + } + } else { + // Include basic variant (current sb if it's the basic one) + if (group.basicId === sb.id) { + filtered.push({ ...sb, paramVisibility: visibility }) + } else { + const basicSb = allSubBlocks.find((s) => s.id === group.basicId) + if (basicSb) { + filtered.push({ ...basicSb, paramVisibility: visibility }) + } + } + } + continue + } + } + + // Non-canonical, non-hidden, condition-passing subblock + filtered.push({ ...sb, paramVisibility: visibility }) + } + + return { + toolConfig, + subBlocks: filtered, + oauthConfig: toolConfig.oauth, + } + } catch (error) { + logger.error('Error getting subblocks for tool input:', error) + return null + } +} diff --git a/apps/sim/tools/pulse/parser.ts b/apps/sim/tools/pulse/parser.ts index 805d998ec0..1828019636 100644 --- a/apps/sim/tools/pulse/parser.ts +++ b/apps/sim/tools/pulse/parser.ts @@ -18,7 +18,7 @@ export const pulseParserTool: ToolConfig = file: { type: 'file', required: false, - visibility: 'hidden', + visibility: 'user-only', description: 'Document file to be processed', }, fileUpload: { @@ -268,7 +268,7 @@ export const pulseParserV2Tool: ToolConfig = { files: { type: 'file[]', required: false, - visibility: 'hidden', + visibility: 'user-only', description: 'Files to upload', }, fileContent: { diff --git a/apps/sim/tools/vision/tool.ts b/apps/sim/tools/vision/tool.ts index 02dba60f29..01d0b93996 100644 --- a/apps/sim/tools/vision/tool.ts +++ b/apps/sim/tools/vision/tool.ts @@ -106,7 +106,7 @@ export const visionToolV2: ToolConfig = { imageFile: { type: 'file', required: true, - visibility: 'hidden', + visibility: 'user-only', description: 'Image file to analyze', }, model: visionTool.params.model, diff --git a/apps/sim/tools/wordpress/upload_media.ts b/apps/sim/tools/wordpress/upload_media.ts index 50bc57eefe..7115346aaa 100644 --- a/apps/sim/tools/wordpress/upload_media.ts +++ b/apps/sim/tools/wordpress/upload_media.ts @@ -27,7 +27,7 @@ export const uploadMediaTool: ToolConfig