From 932dd8c99d41b97dc399db4530728bf713993e81 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:46:52 -0700 Subject: [PATCH 1/7] fix(workflow): render agent tool chip names from static sources, not stored title Tool entries in an agent block's inputs carry a mutable `title` in workflow state, which copilot edits (edit_workflow) could rewrite to change what the UI displays. Derive chip names from static/canonical sources instead: the block registry name for integration tools (raw type id if unregistered), the custom-tool record over the stored snapshot, the live MCP tool name, and a static literal for workflow-as-tool. The stored `title` still persists in state; the UI just no longer reads it for registry-backed tools. Covers both render paths: the expanded tool-input chips and the collapsed canvas summary (resolveToolsLabel, also used by workflow preview). Co-Authored-By: Claude Fable 5 --- .../components/tool-input/tool-input.tsx | 13 ++++++- .../lib/workflows/subblocks/display.test.ts | 24 +++++++++++++ apps/sim/lib/workflows/subblocks/display.ts | 34 ++++++++++--------- 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index 46e7ff7c15e..6b1e4bda5f3 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 @@ -1747,6 +1747,17 @@ export const ToolInput = memo(function ToolInput({ ) : [] + // Display name comes from static sources (registry / canonical records), + // never from the workflow JSON's mutable `title`, so edits to the stored + // state cannot change what the UI shows. + const toolDisplayName = isCustomTool + ? resolvedCustomTool?.title || customToolTitle + : isMcpTool + ? mcpTool?.name || tool.title + : isWorkflowTool + ? 'Workflow' + : toolBlock?.name || tool.type + const useSubBlocks = !isCustomTool && !isMcpTool && subBlocksResult?.subBlocks?.length const displayParams: ToolParameterConfig[] = isCustomTool ? customToolParams @@ -1854,7 +1865,7 @@ export const ToolInput = memo(function ToolInput({ )} - {formatDisplayText((isCustomTool ? customToolTitle : tool.title) ?? '', { + {formatDisplayText(toolDisplayName ?? '', { workflowSearchHighlight: getToolTitleSearchHighlight(toolIndex), })} diff --git a/apps/sim/lib/workflows/subblocks/display.test.ts b/apps/sim/lib/workflows/subblocks/display.test.ts index 305c3cd6dd6..05613000d90 100644 --- a/apps/sim/lib/workflows/subblocks/display.test.ts +++ b/apps/sim/lib/workflows/subblocks/display.test.ts @@ -113,6 +113,30 @@ 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 raw type id for unregistered block-backed tools', () => { + expect( + resolveToolsLabel(toolInput, [{ type: 'not_a_real_block', title: 'Sneaky Title' }], []) + ).toBe('not_a_real_block') + }) + + 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..db49353cf6f 100644 --- a/apps/sim/lib/workflows/subblocks/display.ts +++ b/apps/sim/lib/workflows/subblocks/display.ts @@ -427,10 +427,11 @@ export function resolveVariablesLabel( } /** - * 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. Names come from static + * sources first (block registry, custom-tool records) so that edits to the + * stored entry's `title` cannot change what the UI shows; the stored title + * and inline schema names are only fallbacks for shapes with no canonical + * source (MCP snapshots, legacy entries). */ export function resolveToolsLabel( subBlock: SubBlockConfig | undefined, @@ -445,7 +446,17 @@ export function resolveToolsLabel( if (!tool || typeof tool !== 'object') return null const t = tool as Record - if (typeof t.title === 'string' && t.title) return t.title + 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 t.type + } if (t.type === 'custom-tool' && typeof t.customToolId === 'string') { const customTool = customTools.find((candidate) => candidate.id === t.customToolId) @@ -453,23 +464,14 @@ export function resolveToolsLabel( if (customTool?.schema?.function?.name) return customTool.schema.function.name } + if (typeof t.title === 'string' && t.title) return t.title + 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 }) .filter((name): name is string => !!name) From a31a492050c7d2234a5f8e742c4a0e3f842f9b08 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Tue, 7 Jul 2026 19:05:43 -0700 Subject: [PATCH 2/7] fix(workflow): cover workflow-as-tool and custom-tool fallbacks in static naming Address review findings: resolveToolsLabel now returns the static 'Workflow' label for workflow/workflow_input entries instead of falling through to the stored title, matching the panel chip; the panel's custom-tool name priority now matches resolveToolsLabel (record title, record schema function name, then stored title); and the display-name comment no longer overstates the guarantee. Co-Authored-By: Claude Fable 5 --- .../sub-block/components/tool-input/tool-input.tsx | 14 +++++++------- apps/sim/lib/workflows/subblocks/display.test.ts | 7 +++++++ apps/sim/lib/workflows/subblocks/display.ts | 2 ++ 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index 6b1e4bda5f3..4b42b5352f9 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 @@ -1713,9 +1713,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,11 +1744,14 @@ export const ToolInput = memo(function ToolInput({ ) : [] - // Display name comes from static sources (registry / canonical records), - // never from the workflow JSON's mutable `title`, so edits to the stored - // state cannot change what the UI shows. + // Display name comes from static sources (registry / canonical records) + // where available; the stored `title` is only a last-resort fallback for + // MCP tools while server data is loading and for deleted custom-tool records. const toolDisplayName = isCustomTool - ? resolvedCustomTool?.title || customToolTitle + ? resolvedCustomTool?.title || + resolvedCustomTool?.schema?.function?.name || + tool.title || + 'Unknown Tool' : isMcpTool ? mcpTool?.name || tool.title : isWorkflowTool diff --git a/apps/sim/lib/workflows/subblocks/display.test.ts b/apps/sim/lib/workflows/subblocks/display.test.ts index 05613000d90..03cc1350fc9 100644 --- a/apps/sim/lib/workflows/subblocks/display.test.ts +++ b/apps/sim/lib/workflows/subblocks/display.test.ts @@ -128,6 +128,13 @@ describe('resolveToolsLabel', () => { ).toBe('not_a_real_block') }) + 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 custom tool record over the stored title', () => { expect( resolveToolsLabel( diff --git a/apps/sim/lib/workflows/subblocks/display.ts b/apps/sim/lib/workflows/subblocks/display.ts index db49353cf6f..15ad88dcc6b 100644 --- a/apps/sim/lib/workflows/subblocks/display.ts +++ b/apps/sim/lib/workflows/subblocks/display.ts @@ -458,6 +458,8 @@ export function resolveToolsLabel( return t.type } + if (t.type === 'workflow' || t.type === 'workflow_input') return 'Workflow' + if (t.type === 'custom-tool' && typeof t.customToolId === 'string') { const customTool = customTools.find((candidate) => candidate.id === t.customToolId) if (customTool?.title) return customTool.title From 9e933fc520a765028980826e1a429d9f387982b1 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Tue, 7 Jul 2026 19:34:55 -0700 Subject: [PATCH 3/7] fix(workflow): resolve panel chip names via unfiltered block registry The panel resolved integration tool names through toolBlocks, which is permission- and toolbar-filtered, so a stored tool whose type was filtered out of the picker fell back to the raw type id while the canvas summary (getBlock) still showed the registry name. Use getBlock directly for the display name so both paths agree; toolBlocks still drives the chip icon, params, and picker. Co-Authored-By: Claude Fable 5 --- .../components/sub-block/components/tool-input/tool-input.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/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 4b42b5352f9..f3ab74e3c2e 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 @@ -59,7 +59,7 @@ 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 { getTileIconColorClass } from '@/blocks/icon-color' import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types' import { BUILT_IN_TOOL_TYPES } from '@/blocks/utils' @@ -1756,7 +1756,7 @@ export const ToolInput = memo(function ToolInput({ ? mcpTool?.name || tool.title : isWorkflowTool ? 'Workflow' - : toolBlock?.name || tool.type + : getBlock(tool.type)?.name || tool.type const useSubBlocks = !isCustomTool && !isMcpTool && subBlocksResult?.subBlocks?.length const displayParams: ToolParameterConfig[] = isCustomTool From 4a6fee536f5252b8fd2c9f5eeebe256f25cec600 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Tue, 7 Jul 2026 19:42:22 -0700 Subject: [PATCH 4/7] fix(workflow): resolve MCP tool names from live server data in canvas summaries resolveToolsLabel preferred the stored title for mcp entries, so canvas and preview summaries could show a state-edited name while the config panel showed the live MCP tool name. Accept an optional mcpTools list (matched by toolId, same as the panel) and pass the already-fetched MCP tool data from workflow-block; the stored title remains the fallback while server data is unavailable. Co-Authored-By: Claude Fable 5 --- .../workflow-block/workflow-block.tsx | 8 ++++++-- .../sim/lib/workflows/subblocks/display.test.ts | 14 ++++++++++++++ apps/sim/lib/workflows/subblocks/display.ts | 17 ++++++++++++----- 3 files changed, 32 insertions(+), 7 deletions(-) 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..a7153516bf6 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 @@ -331,9 +331,13 @@ const SubBlockRow = memo(function SubBlockRow({ /** Hydrates tool references to display names. */ const { data: customTools = [] } = useCustomTools(workspaceId || '') + const mcpToolSummaries = useMemo( + () => mcpToolsData.map((t) => ({ id: createMcpToolId(t.serverId, t.name), name: t.name })), + [mcpToolsData] + ) const toolsDisplayValue = useMemo( - () => resolveToolsLabel(subBlock, rawValue, customTools), - [subBlock, rawValue, customTools] + () => resolveToolsLabel(subBlock, rawValue, customTools, mcpToolSummaries), + [subBlock, rawValue, customTools, mcpToolSummaries] ) const filterDisplayValue = useMemo( diff --git a/apps/sim/lib/workflows/subblocks/display.test.ts b/apps/sim/lib/workflows/subblocks/display.test.ts index 03cc1350fc9..d9d3412ccd7 100644 --- a/apps/sim/lib/workflows/subblocks/display.test.ts +++ b/apps/sim/lib/workflows/subblocks/display.test.ts @@ -135,6 +135,20 @@ describe('resolveToolsLabel', () => { 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' }], + [], + [{ id: 'mcp-1', name: '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( diff --git a/apps/sim/lib/workflows/subblocks/display.ts b/apps/sim/lib/workflows/subblocks/display.ts index 15ad88dcc6b..30f8bfd79f3 100644 --- a/apps/sim/lib/workflows/subblocks/display.ts +++ b/apps/sim/lib/workflows/subblocks/display.ts @@ -428,15 +428,17 @@ export function resolveVariablesLabel( /** * Resolves a tool-input value to a tool-name summary. Names come from static - * sources first (block registry, custom-tool records) so that edits to the - * stored entry's `title` cannot change what the UI shows; the stored title - * and inline schema names are only fallbacks for shapes with no canonical - * source (MCP snapshots, legacy entries). + * sources first (block registry, custom-tool records, live MCP tools) so that + * edits to the stored entry's `title` cannot change what the UI shows; the + * stored title and inline schema names are only fallbacks for shapes with no + * canonical source (MCP entries while server data is unavailable, legacy + * entries). */ export function resolveToolsLabel( subBlock: SubBlockConfig | undefined, rawValue: unknown, - customTools: Array<{ id: string; title?: string; schema?: { function?: { name?: string } } }> + customTools: Array<{ id: string; title?: string; schema?: { function?: { name?: string } } }>, + mcpTools: Array<{ id: string; name: string }> = [] ): string | null { if (subBlock?.type !== 'tool-input') return null if (!Array.isArray(rawValue) || rawValue.length === 0) return null @@ -466,6 +468,11 @@ export function resolveToolsLabel( if (customTool?.schema?.function?.name) return customTool.schema.function.name } + if (t.type === 'mcp' && typeof t.toolId === 'string') { + const mcpTool = mcpTools.find((candidate) => candidate.id === t.toolId) + if (mcpTool?.name) return mcpTool.name + } + if (typeof t.title === 'string' && t.title) return t.title const schema = t.schema as { function?: { name?: string } } | undefined From 7327d4f96cc82e897828489c5c01740db6cc423d Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:31:47 -0700 Subject: [PATCH 5/7] refactor(workflow): single shared tool-name resolver; fix overlay, fallback, and search regressions Consolidate the per-surface name logic into one resolveStoredToolName in display.ts (canonical source -> stored title -> raw type id) used by the panel chips, the canvas summary, and the search indexer, fixing the regressions the per-renderer patches introduced: - custom-block tools (async registry overlay) no longer render raw custom_block_ ids pre-hydration or after deletion; the canvas memo and panel picker now subscribe to the overlay version so labels recompute when custom blocks hydrate - unresolvable block types fall back to the stored title before the raw type id, instead of always showing the id - the search indexer now indexes the resolved display name (the title entry was already read-only), so search text and highlights match what the chips render instead of the mutable stored title - redundant hardcoded 'Workflow' labels removed (registry names cover workflow/workflow_input); MCP lookups use a shared Map keyed by composite tool id instead of per-entry array scans; panel chip chrome falls back to the unfiltered registry for picker-hidden types Co-Authored-By: Claude Fable 5 --- .../components/tool-input/tool-input.tsx | 33 ++--- .../workflow-block/workflow-block.tsx | 39 ++++-- .../workflows/search-replace/indexer.test.ts | 7 +- .../lib/workflows/search-replace/indexer.ts | 10 +- .../lib/workflows/subblocks/display.test.ts | 19 ++- apps/sim/lib/workflows/subblocks/display.ts | 120 +++++++++++------- 6 files changed, 143 insertions(+), 85 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index f3ab74e3c2e..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' @@ -60,6 +61,7 @@ import { useActiveSearchTarget, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider' 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 = @@ -1744,19 +1756,10 @@ export const ToolInput = memo(function ToolInput({ ) : [] - // Display name comes from static sources (registry / canonical records) - // where available; the stored `title` is only a last-resort fallback for - // MCP tools while server data is loading and for deleted custom-tool records. - const toolDisplayName = isCustomTool - ? resolvedCustomTool?.title || - resolvedCustomTool?.schema?.function?.name || - tool.title || - 'Unknown Tool' - : isMcpTool - ? mcpTool?.name || tool.title - : isWorkflowTool - ? 'Workflow' - : getBlock(tool.type)?.name || tool.type + // 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 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 a7153516bf6..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,15 +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 mcpToolSummaries = useMemo( - () => mcpToolsData.map((t) => ({ id: createMcpToolId(t.serverId, t.name), name: t.name })), - [mcpToolsData] - ) + const customBlockOverlayVersion = useCustomBlockOverlayVersion() const toolsDisplayValue = useMemo( - () => resolveToolsLabel(subBlock, rawValue, customTools, mcpToolSummaries), - [subBlock, rawValue, customTools, mcpToolSummaries] + () => 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..5d5ce60f600 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) diff --git a/apps/sim/lib/workflows/search-replace/indexer.ts b/apps/sim/lib/workflows/search-replace/indexer.ts index 2a412fedd6c..92a2cf7b4ea 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, @@ -954,7 +955,12 @@ function addToolInputMatches({ 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, { + getBlockConfig: (type) => blockConfigs?.[type] ?? getBlock(type), + }) + if (mode !== 'resource' && toolDisplayName) { addTextMatches({ matches, idPrefix: 'tool-input-title', @@ -963,7 +969,7 @@ function addToolInputMatches({ canonicalSubBlockId, subBlockType: 'tool-input', fieldTitle: 'Tool', - value: tool.title, + value: toolDisplayName, valuePath: [toolIndex, 'title'], target: { kind: 'subblock' }, query, diff --git a/apps/sim/lib/workflows/subblocks/display.test.ts b/apps/sim/lib/workflows/subblocks/display.test.ts index d9d3412ccd7..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 { @@ -122,10 +126,13 @@ describe('resolveToolsLabel', () => { ) }) - it('falls back to the raw type id for unregistered block-backed tools', () => { + it('falls back to the stored title, then the raw type id, for unresolvable block types', () => { expect( - resolveToolsLabel(toolInput, [{ type: 'not_a_real_block', title: 'Sneaky Title' }], []) - ).toBe('not_a_real_block') + 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', () => { @@ -141,11 +148,11 @@ describe('resolveToolsLabel', () => { toolInput, [{ type: 'mcp', toolId: 'mcp-1', title: 'Renamed By Copilot' }], [], - [{ id: 'mcp-1', name: 'Live MCP Name' }] + new Map([['mcp-1', 'Live MCP Name']]) ) ).toBe('Live MCP Name') expect( - resolveToolsLabel(toolInput, [{ type: 'mcp', toolId: 'mcp-1', title: 'Snapshot' }], [], []) + resolveToolsLabel(toolInput, [{ type: 'mcp', toolId: 'mcp-1', title: 'Snapshot' }], []) ).toBe('Snapshot') }) diff --git a/apps/sim/lib/workflows/subblocks/display.ts b/apps/sim/lib/workflows/subblocks/display.ts index 30f8bfd79f3..26088c25fff 100644 --- a/apps/sim/lib/workflows/subblocks/display.ts +++ b/apps/sim/lib/workflows/subblocks/display.ts @@ -426,63 +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 a tool-input value to a tool-name summary. Names come from static - * sources first (block registry, custom-tool records, live MCP tools) so that - * edits to the stored entry's `title` cannot change what the UI shows; the - * stored title and inline schema names are only fallbacks for shapes with no - * canonical source (MCP entries while server data is unavailable, legacy - * entries). + * 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 resolveToolsLabel( - subBlock: SubBlockConfig | undefined, - rawValue: unknown, - customTools: Array<{ id: string; title?: string; schema?: { function?: { name?: string } } }>, - mcpTools: Array<{ id: string; name: string }> = [] +export function resolveStoredToolName( + tool: unknown, + options: ResolveStoredToolNameOptions = {} ): 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.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 t.type - } + 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 === 'workflow' || t.type === 'workflow_input') return 'Workflow' + if (t.type === 'mcp') { + if (typeof t.toolId === 'string') { + const liveName = mcpToolNamesById?.get(t.toolId) + if (liveName) return liveName + } + return storedTitle + } - 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 - } + if (typeof t.type === 'string' && t.type) { + const blockConfig = getBlockConfig(t.type) + if (blockConfig?.name) return blockConfig.name + return storedTitle || t.type + } - if (t.type === 'mcp' && typeof t.toolId === 'string') { - const mcpTool = mcpTools.find((candidate) => candidate.id === t.toolId) - if (mcpTool?.name) return mcpTool.name - } + if (storedTitle) return storedTitle + if (schemaName) return schemaName - if (typeof t.title === 'string' && t.title) return t.title + const fn = t.function as { name?: string } | undefined + if (fn?.name) return fn.name - const schema = t.schema as { function?: { name?: string } } | undefined - if (schema?.function?.name) return schema.function.name + return null +} - const fn = t.function as { name?: string } | undefined - if (fn?.name) return fn.name +/** + * 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: StoredCustomToolRecord[], + mcpToolNamesById?: ReadonlyMap +): string | null { + if (subBlock?.type !== 'tool-input') return null + if (!Array.isArray(rawValue) || rawValue.length === 0) return null - return null - }) + const names = rawValue + .map((tool: unknown) => resolveStoredToolName(tool, { customTools, mcpToolNamesById })) .filter((name): name is string => !!name) return summarizeNames(names) From f768b00e0bd816e6c12e5a9a92874b1a6fdbb7b9 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:12:29 -0700 Subject: [PATCH 6/7] fix(workflow): thread custom-tool and MCP data into the search indexer; fix blockConfigs type The search index now resolves tool names with the same data as the chip renderers: the single caller passes customTools and a live MCP name map into indexWorkflowSearchMatches, so indexed names match the panel for custom-tool references and MCP entries, not just registry-backed types. Also declare the optional name on WorkflowSearchIndexerOptions blockConfigs entries, fixing the next build type error. Co-Authored-By: Claude Fable 5 --- .../workflow-search-replace.tsx | 15 +++++++ .../workflows/search-replace/indexer.test.ts | 44 +++++++++++++++++++ .../lib/workflows/search-replace/indexer.ts | 10 +++++ .../sim/lib/workflows/search-replace/types.ts | 12 ++++- 4 files changed, 80 insertions(+), 1 deletion(-) 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..61520335f7f 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,9 @@ 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 { 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 +172,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({ @@ -215,10 +226,14 @@ export function WorkflowSearchReplace() { workspaceId, workflowId, credentialTypeById, + customTools, + mcpToolNamesById, }), [ currentWorkflow.isSnapshotView, credentialTypeById, + customTools, + mcpToolNamesById, query, readonlyReason, searchBlocks, diff --git a/apps/sim/lib/workflows/search-replace/indexer.test.ts b/apps/sim/lib/workflows/search-replace/indexer.test.ts index 5d5ce60f600..2fe792b2073 100644 --- a/apps/sim/lib/workflows/search-replace/indexer.test.ts +++ b/apps/sim/lib/workflows/search-replace/indexer.test.ts @@ -1423,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 92a2cf7b4ea..359dc8e7c01 100644 --- a/apps/sim/lib/workflows/search-replace/indexer.ts +++ b/apps/sim/lib/workflows/search-replace/indexer.ts @@ -932,6 +932,8 @@ function addToolInputMatches({ workflowId, credentialTypeById, blockConfigs, + customTools, + mcpToolNamesById, }: { matches: WorkflowSearchMatch[] block: WorkflowSearchBlockState @@ -951,6 +953,8 @@ function addToolInputMatches({ workflowId?: string credentialTypeById?: Record blockConfigs?: WorkflowSearchIndexerOptions['blockConfigs'] + customTools?: WorkflowSearchIndexerOptions['customTools'] + mcpToolNamesById?: WorkflowSearchIndexerOptions['mcpToolNamesById'] }) { const parentCanonicalModes = getSearchCanonicalModes(block) @@ -958,6 +962,8 @@ function addToolInputMatches({ // 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) { @@ -1234,6 +1240,8 @@ export function indexWorkflowSearchMatches( workflowId, blockConfigs = {}, credentialTypeById, + customTools, + mcpToolNamesById, } = options const matches: WorkflowSearchMatch[] = [] @@ -1369,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 { From d76e45d436d4ae8abd425c275d28f846b04071e3 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:20:03 -0700 Subject: [PATCH 7/7] fix(workflow): recompute search index when the custom-block overlay hydrates The matches memo resolves tool names through getBlock, so it must carry the overlay version dep like the other getBlock-derived memos; without it, find-in-workflow could index stale custom-block names until an unrelated dep changed. Co-Authored-By: Claude Fable 5 --- .../components/search-replace/workflow-search-replace.tsx | 4 ++++ 1 file changed, 4 insertions(+) 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 61520335f7f..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,6 +35,7 @@ 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' @@ -213,6 +214,8 @@ export function WorkflowSearchReplace() { [workspaceCredentials] ) + /** Overlay version invalidates getBlock-resolved tool names in the index. */ + const customBlockOverlayVersion = useCustomBlockOverlayVersion() const matches = useMemo( () => indexWorkflowSearchMatches({ @@ -232,6 +235,7 @@ export function WorkflowSearchReplace() { [ currentWorkflow.isSnapshotView, credentialTypeById, + customBlockOverlayVersion, customTools, mcpToolNamesById, query,