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 46e7ff7c15e..6511dde22ef 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 @@ -27,6 +27,7 @@ import { import type { McpToolSchema } from '@/lib/mcp/types' import { getProviderIdFromServiceId, type OAuthProvider, type OAuthService } from '@/lib/oauth' import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format' +import { resolveStoredToolName } from '@/lib/workflows/subblocks/display' import { buildToolSubBlockId } from '@/lib/workflows/tool-input/synthetic-subblocks' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { McpServerFormModal } from '@/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal' @@ -59,7 +60,8 @@ import { ActiveSearchTargetProvider, useActiveSearchTarget, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider' -import { getAllBlocks } from '@/blocks' +import { getAllBlocks, getBlock } from '@/blocks' +import { useCustomBlockOverlayVersion } from '@/blocks/custom/client-overlay' import { getTileIconColorClass } from '@/blocks/icon-color' import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types' import { BUILT_IN_TOOL_TYPES } from '@/blocks/utils' @@ -543,6 +545,13 @@ export const ToolInput = memo(function ToolInput({ const { data: customTools = [] } = useCustomTools(shouldFetchCustomTools ? workspaceId : '') const { mcpTools, isLoading: mcpLoading } = useMcpTools(workspaceId) + const mcpToolNamesById = useMemo(() => { + const names = new Map() + for (const t of mcpTools) { + if (!names.has(t.id)) names.set(t.id, t.name) + } + return names + }, [mcpTools]) const { data: mcpServers = [], isLoading: mcpServersLoading } = useMcpServers(workspaceId) const { data: storedMcpTools = [] } = useStoredMcpTools(workspaceId) @@ -642,6 +651,7 @@ export const ToolInput = memo(function ToolInput({ const { filterBlocks, config: permissionConfig } = usePermissionConfig() + const customBlockOverlayVersion = useCustomBlockOverlayVersion() const toolBlocks = useMemo(() => { const allToolBlocks = getAllBlocks().filter( (block) => @@ -659,7 +669,7 @@ export const ToolInput = memo(function ToolInput({ block.type !== 'file' ) return filterBlocks(allToolBlocks) - }, [filterBlocks]) + }, [filterBlocks, customBlockOverlayVersion]) const hasBackfilledRef = useRef(false) useEffect(() => { @@ -1662,9 +1672,11 @@ export const ToolInput = memo(function ToolInput({ const isCustomTool = tool.type === 'custom-tool' const isMcpTool = tool.type === 'mcp' const isWorkflowTool = tool.type === 'workflow' + // Fall back to the unfiltered registry so chips for types hidden + // from the picker (permissions, hideFromToolbar) keep their chrome. const toolBlock = !isCustomTool && !isMcpTool - ? toolBlocks.find((block) => block.type === tool.type) + ? (toolBlocks.find((block) => block.type === tool.type) ?? getBlock(tool.type)) : null const currentToolId = @@ -1713,9 +1725,6 @@ export const ToolInput = memo(function ToolInput({ ? resolveCustomToolFromReference(tool, customTools) : null - const customToolTitle = isCustomTool - ? tool.title || resolvedCustomTool?.title || 'Unknown Tool' - : null const customToolSchema = isCustomTool ? tool.schema || resolvedCustomTool?.schema : null const customToolParams = isCustomTool && customToolSchema?.function?.parameters?.properties @@ -1747,6 +1756,11 @@ export const ToolInput = memo(function ToolInput({ ) : [] + // Canonical name wins; stored title only when nothing resolves + // (same policy as the canvas summary — see resolveStoredToolName). + const toolDisplayName = + resolveStoredToolName(tool, { customTools, mcpToolNamesById }) ?? 'Unknown Tool' + const useSubBlocks = !isCustomTool && !isMcpTool && subBlocksResult?.subBlocks?.length const displayParams: ToolParameterConfig[] = isCustomTool ? customToolParams @@ -1854,7 +1868,7 @@ export const ToolInput = memo(function ToolInput({ )} - {formatDisplayText((isCustomTool ? customToolTitle : tool.title) ?? '', { + {formatDisplayText(toolDisplayName ?? '', { workflowSearchHighlight: getToolTitleSearchHighlight(toolIndex), })} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx index c5558e59807..650170cae4a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/search-replace/workflow-search-replace.tsx @@ -35,7 +35,10 @@ import { } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/float' import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow' import { getBlock } from '@/blocks' +import { useCustomBlockOverlayVersion } from '@/blocks/custom/client-overlay' +import { useMcpTools } from '@/hooks/mcp/use-mcp-tools' import { useWorkspaceCredentials } from '@/hooks/queries/credentials' +import { useCustomTools } from '@/hooks/queries/custom-tools' import { useFolderMap } from '@/hooks/queries/folders' import { isWorkflowEffectivelyLocked } from '@/hooks/queries/utils/folder-tree' import { useWorkflowMap } from '@/hooks/queries/workflows' @@ -170,6 +173,15 @@ export function WorkflowSearchReplace() { const prevIsOpenRef = useRef(false) const afterReplaceIndexRef = useRef(null) const { data: workspaceCredentials } = useWorkspaceCredentials({ workspaceId, enabled: isOpen }) + const { data: customTools = [] } = useCustomTools(isOpen && workspaceId ? workspaceId : '') + const { mcpTools } = useMcpTools(isOpen && workspaceId ? workspaceId : '') + const mcpToolNamesById = useMemo(() => { + const names = new Map() + for (const t of mcpTools) { + if (!names.has(t.id)) names.set(t.id, t.name) + } + return names + }, [mcpTools]) useRegisterGlobalCommands([ createCommand({ @@ -202,6 +214,8 @@ export function WorkflowSearchReplace() { [workspaceCredentials] ) + /** Overlay version invalidates getBlock-resolved tool names in the index. */ + const customBlockOverlayVersion = useCustomBlockOverlayVersion() const matches = useMemo( () => indexWorkflowSearchMatches({ @@ -215,10 +229,15 @@ export function WorkflowSearchReplace() { workspaceId, workflowId, credentialTypeById, + customTools, + mcpToolNamesById, }), [ currentWorkflow.isSnapshotView, credentialTypeById, + customBlockOverlayVersion, + customTools, + mcpToolNamesById, query, readonlyReason, searchBlocks, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index 133bf3a26f8..3e63939ba9e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -44,6 +44,7 @@ import { } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/utils' import { useBlockVisual } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' import { useBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions' +import { useCustomBlockOverlayVersion } from '@/blocks/custom/client-overlay' import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types' import { getDependsOnFields } from '@/blocks/utils' import { useKnowledgeBase } from '@/hooks/kb/use-knowledge' @@ -68,6 +69,9 @@ const logger = createLogger('WorkflowBlock') /** Stable empty object to avoid creating new references */ const EMPTY_SUBBLOCK_VALUES = {} as Record +/** Stable empty map for rows that never resolve MCP tool names */ +const EMPTY_MCP_TOOL_NAMES: ReadonlyMap = new Map() + interface SubBlockRowProps { title: string value?: string @@ -274,17 +278,23 @@ const SubBlockRow = memo(function SubBlockRow({ }, [subBlock?.type, rawValue, mcpServers]) const { data: mcpToolsData = [] } = useMcpToolsQuery(workspaceId || '') + const mcpToolNamesById = useMemo(() => { + if (subBlock?.type !== 'mcp-tool-selector' && subBlock?.type !== 'tool-input') { + return EMPTY_MCP_TOOL_NAMES + } + const names = new Map() + for (const t of mcpToolsData) { + const toolId = createMcpToolId(t.serverId, t.name) + if (!names.has(toolId)) names.set(toolId, t.name) + } + return names + }, [subBlock?.type, mcpToolsData]) const mcpToolDisplayName = useMemo(() => { if (subBlock?.type !== 'mcp-tool-selector' || typeof rawValue !== 'string') { return null } - - const tool = mcpToolsData.find((t) => { - const toolId = createMcpToolId(t.serverId, t.name) - return toolId === rawValue - }) - return tool?.name ?? null - }, [subBlock?.type, rawValue, mcpToolsData]) + return mcpToolNamesById.get(rawValue) ?? null + }, [subBlock?.type, rawValue, mcpToolNamesById]) const { data: tables = [] } = useTablesList(workspaceId || '') const tableDisplayName = useMemo(() => { @@ -329,11 +339,16 @@ const SubBlockRow = memo(function SubBlockRow({ [subBlock, rawValue, workflowVariables] ) - /** Hydrates tool references to display names. */ + /** + * Hydrates tool references to display names. The overlay version is a dep + * because resolveToolsLabel reads getBlock, whose custom-block results + * change when the client overlay hydrates (see client-overlay.ts). + */ const { data: customTools = [] } = useCustomTools(workspaceId || '') + const customBlockOverlayVersion = useCustomBlockOverlayVersion() const toolsDisplayValue = useMemo( - () => resolveToolsLabel(subBlock, rawValue, customTools), - [subBlock, rawValue, customTools] + () => resolveToolsLabel(subBlock, rawValue, customTools, mcpToolNamesById), + [subBlock, rawValue, customTools, mcpToolNamesById, customBlockOverlayVersion] ) const filterDisplayValue = useMemo( diff --git a/apps/sim/lib/workflows/search-replace/indexer.test.ts b/apps/sim/lib/workflows/search-replace/indexer.test.ts index 082cee6de72..2fe792b2073 100644 --- a/apps/sim/lib/workflows/search-replace/indexer.test.ts +++ b/apps/sim/lib/workflows/search-replace/indexer.test.ts @@ -1387,6 +1387,10 @@ describe('indexWorkflowSearchMatches', () => { custom: { subBlocks: [{ id: 'tools', title: 'Tools', type: 'tool-input' }], }, + native: { + name: 'Customer Mailer', + subBlocks: [], + }, }, }).filter((match) => match.blockId === 'tool-input-1') @@ -1395,7 +1399,7 @@ describe('indexWorkflowSearchMatches', () => { expect.objectContaining({ subBlockId: 'tools', valuePath: [0, 'title'], - searchText: 'Customer notifier', + searchText: 'Customer Mailer', }), expect.objectContaining({ subBlockId: 'tools', @@ -1404,6 +1408,7 @@ describe('indexWorkflowSearchMatches', () => { }), ]) ) + expect(matches.some((match) => match.searchText === 'Customer notifier')).toBe(false) expect(matches.some((match) => match.valuePath.includes('toolId'))).toBe(false) expect(matches.some((match) => match.valuePath.includes('operation'))).toBe(false) expect(matches.some((match) => match.valuePath.includes('credentialId'))).toBe(false) @@ -1418,6 +1423,50 @@ describe('indexWorkflowSearchMatches', () => { expect(matches.some((match) => match.valuePath.includes('schema'))).toBe(false) }) + it('indexes canonical MCP and custom-tool names over mutated stored titles', () => { + const workflow = createSearchReplaceWorkflowFixture() + workflow.blocks['tool-input-1'] = { + id: 'tool-input-1', + type: 'custom', + name: 'Tool Input Block', + position: { x: 0, y: 0 }, + enabled: true, + outputs: {}, + subBlocks: { + tools: { + id: 'tools', + type: 'tool-input', + value: [ + { type: 'mcp', toolId: 'mcp-tool-1', title: 'Mutated Title', params: {} }, + { type: 'custom-tool', customToolId: 'ct-1', title: 'Mutated Title', params: {} }, + ], + }, + }, + } + + const matches = indexWorkflowSearchMatches({ + workflow, + query: 'customer', + mode: 'text', + blockConfigs: { + ...SEARCH_REPLACE_BLOCK_CONFIGS, + custom: { + subBlocks: [{ id: 'tools', title: 'Tools', type: 'tool-input' }], + }, + }, + customTools: [{ id: 'ct-1', title: 'Customer Records' }], + mcpToolNamesById: new Map([['mcp-tool-1', 'customer_lookup']]), + }).filter((match) => match.blockId === 'tool-input-1') + + expect(matches).toEqual( + expect.arrayContaining([ + expect.objectContaining({ valuePath: [0, 'title'], searchText: 'customer_lookup' }), + expect.objectContaining({ valuePath: [1, 'title'], searchText: 'Customer Records' }), + ]) + ) + expect(matches.some((match) => match.searchText === 'Mutated Title')).toBe(false) + }) + it('indexes explicit secret tool params for intentional replacement', () => { const workflow = createSearchReplaceWorkflowFixture() workflow.blocks['tool-input-1'] = { diff --git a/apps/sim/lib/workflows/search-replace/indexer.ts b/apps/sim/lib/workflows/search-replace/indexer.ts index 2a412fedd6c..359dc8e7c01 100644 --- a/apps/sim/lib/workflows/search-replace/indexer.ts +++ b/apps/sim/lib/workflows/search-replace/indexer.ts @@ -23,6 +23,7 @@ import type { } from '@/lib/workflows/search-replace/types' import { pathToKey, walkStringValues } from '@/lib/workflows/search-replace/value-walker' import { SELECTOR_CONTEXT_FIELDS } from '@/lib/workflows/subblocks/context' +import { resolveStoredToolName } from '@/lib/workflows/subblocks/display' import { buildCanonicalIndex, buildSubBlockValues, @@ -931,6 +932,8 @@ function addToolInputMatches({ workflowId, credentialTypeById, blockConfigs, + customTools, + mcpToolNamesById, }: { matches: WorkflowSearchMatch[] block: WorkflowSearchBlockState @@ -950,11 +953,20 @@ function addToolInputMatches({ workflowId?: string credentialTypeById?: Record blockConfigs?: WorkflowSearchIndexerOptions['blockConfigs'] + customTools?: WorkflowSearchIndexerOptions['customTools'] + mcpToolNamesById?: WorkflowSearchIndexerOptions['mcpToolNamesById'] }) { const parentCanonicalModes = getSearchCanonicalModes(block) parseStoredToolInputValue(value).forEach((tool, toolIndex) => { - if (mode !== 'resource' && tool.title) { + // Index the resolved display name (not the stored mutable title) so + // search text and highlights match what the tool chip actually renders. + const toolDisplayName = resolveStoredToolName(tool, { + customTools, + mcpToolNamesById, + getBlockConfig: (type) => blockConfigs?.[type] ?? getBlock(type), + }) + if (mode !== 'resource' && toolDisplayName) { addTextMatches({ matches, idPrefix: 'tool-input-title', @@ -963,7 +975,7 @@ function addToolInputMatches({ canonicalSubBlockId, subBlockType: 'tool-input', fieldTitle: 'Tool', - value: tool.title, + value: toolDisplayName, valuePath: [toolIndex, 'title'], target: { kind: 'subblock' }, query, @@ -1228,6 +1240,8 @@ export function indexWorkflowSearchMatches( workflowId, blockConfigs = {}, credentialTypeById, + customTools, + mcpToolNamesById, } = options const matches: WorkflowSearchMatch[] = [] @@ -1363,6 +1377,8 @@ export function indexWorkflowSearchMatches( workflowId, credentialTypeById, blockConfigs, + customTools, + mcpToolNamesById, }) continue } diff --git a/apps/sim/lib/workflows/search-replace/types.ts b/apps/sim/lib/workflows/search-replace/types.ts index 9298430318d..4eefe464a80 100644 --- a/apps/sim/lib/workflows/search-replace/types.ts +++ b/apps/sim/lib/workflows/search-replace/types.ts @@ -3,6 +3,7 @@ import type { WorkflowSearchSubflowEditableValue, WorkflowSearchSubflowFieldId, } from '@/lib/workflows/search-replace/subflow-fields' +import type { StoredCustomToolRecord } from '@/lib/workflows/subblocks/display' import type { SubBlockConfig } from '@/blocks/types' import type { SelectorContext } from '@/hooks/selectors/types' import type { BlockState, SubBlockState } from '@/stores/workflows/workflow/types' @@ -96,10 +97,19 @@ export interface WorkflowSearchIndexerOptions { workflowId?: string blockConfigs?: Record< string, - | { subBlocks?: SubBlockConfig[]; triggers?: { enabled?: boolean }; category?: string } + | { + name?: string + subBlocks?: SubBlockConfig[] + triggers?: { enabled?: boolean } + category?: string + } | undefined > credentialTypeById?: Record + /** Custom-tool records so indexed tool names match the rendered chips. */ + customTools?: StoredCustomToolRecord[] + /** Live MCP tool names keyed by composite tool id, same as the chip renderers. */ + mcpToolNamesById?: ReadonlyMap } export interface WorkflowSearchReplacementOption { diff --git a/apps/sim/lib/workflows/subblocks/display.test.ts b/apps/sim/lib/workflows/subblocks/display.test.ts index 305c3cd6dd6..310af5d3701 100644 --- a/apps/sim/lib/workflows/subblocks/display.test.ts +++ b/apps/sim/lib/workflows/subblocks/display.test.ts @@ -4,7 +4,11 @@ import { describe, expect, it, vi } from 'vitest' vi.mock('@/blocks', () => ({ - getBlock: (type: string) => (type === 'slack' ? { name: 'Slack' } : undefined), + getBlock: (type: string) => { + if (type === 'slack') return { name: 'Slack' } + if (type === 'workflow' || type === 'workflow_input') return { name: 'Workflow' } + return undefined + }, })) import { @@ -113,6 +117,54 @@ describe('resolveToolsLabel', () => { null ) }) + + it('ignores the stored title on registry-backed tools so state edits cannot rename them', () => { + const slackName = resolveToolsLabel(toolInput, [{ type: 'slack' }], []) + expect(slackName).not.toBe(null) + expect(resolveToolsLabel(toolInput, [{ type: 'slack', title: 'Renamed By Copilot' }], [])).toBe( + slackName + ) + }) + + it('falls back to the stored title, then the raw type id, for unresolvable block types', () => { + expect( + resolveToolsLabel(toolInput, [{ type: 'custom_block_gone', title: 'Invoice Parser' }], []) + ).toBe('Invoice Parser') + expect(resolveToolsLabel(toolInput, [{ type: 'custom_block_gone' }], [])).toBe( + 'custom_block_gone' + ) + }) + + it('renders the static label for workflow-as-tool entries regardless of stored title', () => { + expect( + resolveToolsLabel(toolInput, [{ type: 'workflow', title: 'Renamed By Copilot' }], []) + ).toBe('Workflow') + expect(resolveToolsLabel(toolInput, [{ type: 'workflow_input' }], [])).toBe('Workflow') + }) + + it('prefers the live MCP tool name over the stored title', () => { + expect( + resolveToolsLabel( + toolInput, + [{ type: 'mcp', toolId: 'mcp-1', title: 'Renamed By Copilot' }], + [], + new Map([['mcp-1', 'Live MCP Name']]) + ) + ).toBe('Live MCP Name') + expect( + resolveToolsLabel(toolInput, [{ type: 'mcp', toolId: 'mcp-1', title: 'Snapshot' }], []) + ).toBe('Snapshot') + }) + + it('prefers the custom tool record over the stored title', () => { + expect( + resolveToolsLabel( + toolInput, + [{ type: 'custom-tool', customToolId: 'ct-1', title: 'Renamed By Copilot' }], + [{ id: 'ct-1', title: 'My Tool' }] + ) + ).toBe('My Tool') + }) }) describe('resolveSkillsLabel', () => { diff --git a/apps/sim/lib/workflows/subblocks/display.ts b/apps/sim/lib/workflows/subblocks/display.ts index 1bb87a36f71..26088c25fff 100644 --- a/apps/sim/lib/workflows/subblocks/display.ts +++ b/apps/sim/lib/workflows/subblocks/display.ts @@ -426,52 +426,89 @@ export function resolveVariablesLabel( return summarizeNames(names) } +/** Custom-tool record shape needed to resolve a stored custom-tool reference. */ +export interface StoredCustomToolRecord { + id: string + title?: string + schema?: { function?: { name?: string } } +} + +export interface ResolveStoredToolNameOptions { + customTools?: StoredCustomToolRecord[] + /** Live MCP tool names keyed by composite tool id (`createMcpToolId`). */ + mcpToolNamesById?: ReadonlyMap + /** Block-config lookup; overridable so snapshot views can scope configs. */ + getBlockConfig?: (type: string) => { name?: string } | undefined +} + +/** + * Resolves one stored tool entry to its display name. Canonical sources win — + * the block registry (including the custom-block overlay), the custom-tool + * record, live MCP server data — so edits to the entry's mutable `title` in + * workflow state cannot relabel a tool that has a canonical name. The stored + * title is the fallback for entries with no resolvable source (deleted custom + * blocks, MCP entries while server data is unavailable, legacy inline custom + * tools where the title is the identity), ahead of the raw type id. + */ +export function resolveStoredToolName( + tool: unknown, + options: ResolveStoredToolNameOptions = {} +): string | null { + if (!tool || typeof tool !== 'object') return null + const t = tool as Record + const { customTools = [], mcpToolNamesById, getBlockConfig = getBlock } = options + + const storedTitle = typeof t.title === 'string' && t.title ? t.title : null + const schema = t.schema as { function?: { name?: string } } | undefined + const schemaName = schema?.function?.name || null + + if (t.type === 'custom-tool') { + if (typeof t.customToolId === 'string') { + const record = customTools.find((candidate) => candidate.id === t.customToolId) + if (record?.title) return record.title + if (record?.schema?.function?.name) return record.schema.function.name + } + return storedTitle || schemaName + } + + if (t.type === 'mcp') { + if (typeof t.toolId === 'string') { + const liveName = mcpToolNamesById?.get(t.toolId) + if (liveName) return liveName + } + return storedTitle + } + + if (typeof t.type === 'string' && t.type) { + const blockConfig = getBlockConfig(t.type) + if (blockConfig?.name) return blockConfig.name + return storedTitle || t.type + } + + if (storedTitle) return storedTitle + if (schemaName) return schemaName + + const fn = t.function as { name?: string } | undefined + if (fn?.name) return fn.name + + return null +} + /** - * Resolves a tool-input value to a tool-name summary. Stored tool entries - * come in several historical shapes, checked in priority order: explicit - * title, custom tool referenced by id, inline schema name, OpenAI function - * name, then the block registry. + * Resolves a tool-input value to a tool-name summary via + * {@link resolveStoredToolName}. Unresolvable entries are skipped. */ export function resolveToolsLabel( subBlock: SubBlockConfig | undefined, rawValue: unknown, - customTools: Array<{ id: string; title?: string; schema?: { function?: { name?: string } } }> + customTools: StoredCustomToolRecord[], + mcpToolNamesById?: ReadonlyMap ): string | null { if (subBlock?.type !== 'tool-input') return null if (!Array.isArray(rawValue) || rawValue.length === 0) return null const names = rawValue - .map((tool: unknown) => { - if (!tool || typeof tool !== 'object') return null - const t = tool as Record - - if (typeof t.title === 'string' && t.title) return t.title - - if (t.type === 'custom-tool' && typeof t.customToolId === 'string') { - const customTool = customTools.find((candidate) => candidate.id === t.customToolId) - if (customTool?.title) return customTool.title - if (customTool?.schema?.function?.name) return customTool.schema.function.name - } - - const schema = t.schema as { function?: { name?: string } } | undefined - if (schema?.function?.name) return schema.function.name - - const fn = t.function as { name?: string } | undefined - if (fn?.name) return fn.name - - if ( - typeof t.type === 'string' && - t.type !== 'custom-tool' && - t.type !== 'mcp' && - t.type !== 'workflow' && - t.type !== 'workflow_input' - ) { - const blockConfig = getBlock(t.type) - if (blockConfig?.name) return blockConfig.name - } - - return null - }) + .map((tool: unknown) => resolveStoredToolName(tool, { customTools, mcpToolNamesById })) .filter((name): name is string => !!name) return summarizeNames(names)