diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input-expansion.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input-expansion.test.tsx new file mode 100644 index 00000000000..d3270f90c2b --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input-expansion.test.tsx @@ -0,0 +1,364 @@ +/** @vitest-environment jsdom */ +import { act, type ComponentProps, type ReactNode, useState } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { create } from 'zustand' +import type { StoredTool } from '@/lib/workflows/tool-input/types' +import type { BlockConfig, SubBlockConfig } from '@/blocks/types' + +const fixture = vi.hoisted(() => ({ + tools: [] as StoredTool[], + target: null as { subBlockId: string; valuePath: (string | number)[] } | null, + write: vi.fn(), + canonical: vi.fn(), + blocks: [] as BlockConfig[], + replace: (_tools: StoredTool[]) => {}, +})) + +vi.mock('next/navigation', () => ({ + useParams: () => ({ workspaceId: 'workspace-1', workflowId: 'workflow-1' }), +})) +vi.mock('@/blocks', () => ({ + getAllBlocks: () => fixture.blocks, + getBlock: (type: string) => fixture.blocks.find((block) => block.type === type), +})) +vi.mock('@/blocks/custom/client-overlay', () => ({ useCustomBlockOverlayVersion: () => 0 })) +vi.mock('@/blocks/utils', () => ({ BUILT_IN_TOOL_TYPES: new Set() })) +vi.mock('@/tools/metadata', () => ({ getToolMetadata: () => undefined })) +vi.mock('@/providers/models', () => ({ supportsForcedToolUse: () => false })) +vi.mock('@/providers/utils', () => ({ + getProviderFromModel: () => '', + supportsToolUsageControl: () => false, +})) +vi.mock('@/hooks/use-collaborative-workflow', () => ({ + useCollaborativeWorkflow: () => ({ + collaborativeSetBlockCanonicalMode: fixture.canonical, + collaborativeSetBlockCanonicalModes: fixture.canonical, + }), +})) +vi.mock('@/hooks/use-permission-config', () => ({ + usePermissionConfig: () => ({ + filterBlocks: (blocks: BlockConfig[]) => blocks, + config: {}, + isLoading: false, + }), +})) +vi.mock('@/hooks/use-operation-access', () => ({ + useOperationAccess: () => ({ getDeniedOperations: () => new Set() }), +})) +vi.mock('@/hooks/queries/custom-tools', () => ({ useCustomTools: () => ({ data: [] }) })) +vi.mock('@/hooks/queries/credentials', () => ({ useWorkspaceCredential: () => ({}) })) +vi.mock('@/hooks/queries/workflows', () => ({ useWorkflows: () => ({ data: [] }) })) +vi.mock('@/hooks/queries/deployments', () => ({ + useDeploymentInfo: () => ({ data: { isDeployed: true } }), + useDeployWorkflow: () => ({}), +})) +vi.mock('@/hooks/mcp/use-mcp-tools', () => ({ + useMcpTools: () => ({ mcpTools: [], isLoading: false }), +})) +vi.mock('@/hooks/queries/mcp', () => ({ + useMcpToolServers: () => ({ data: [] }), + useStoredMcpTools: () => ({ data: [] }), + useAllowedMcpDomains: () => ({}), + useCreateMcpServer: () => ({}), + useForceRefreshMcpTools: () => ({ mutate: () => {} }), +})) +vi.mock('@/hooks/mcp/use-mcp-oauth-popup', () => ({ useMcpOauthPopup: () => ({}) })) +vi.mock('@/hooks/use-available-env-vars', () => ({ useAvailableEnvVarKeys: () => [] })) +vi.mock('@/hooks/use-settings-navigation', () => ({ + useSettingsNavigation: () => ({ navigateToSettings: () => {} }), +})) +vi.mock('@/app/workspace/[workspaceId]/providers/workspace-permissions-provider', () => ({ + useUserPermissionsContext: () => ({ canAdmin: false }), +})) +vi.mock( + '@/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal', + () => ({ McpServerFormModal: () => null }) +) +vi.mock( + '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal', + () => ({ CustomToolModal: () => null }) +) +vi.mock( + '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text', + () => ({ formatDisplayText: (text: string) => text }) +) +vi.mock( + '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight', + () => ({ getActiveWorkflowSearchHighlight: () => null }) +) +vi.mock( + '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider', + () => ({ + useActiveSearchTarget: () => fixture.target, + ActiveSearchTargetProvider: ({ children }: { children: ReactNode }) => children, + }) +) +vi.mock( + '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value', + () => ({ + useSubBlockValue: () => { + const [value, setValue] = useState(fixture.tools) + fixture.replace = setValue + return [ + value, + (tools: StoredTool[]) => { + fixture.write(tools) + fixture.tools = structuredClone(tools) + setValue(fixture.tools) + }, + ] + }, + }) +) +vi.mock('@/stores/workflows/workflow/store', () => ({ + useWorkflowStore: (selector: (state: unknown) => unknown) => + selector({ blocks: { 'block-1': { type: 'agent' } } }), +})) +vi.mock('@/stores/workflows/registry/store', () => ({ + useWorkflowRegistry: Object.assign(() => 'workflow-1', { + getState: () => ({ activeWorkflowId: 'workflow-1' }), + }), +})) +vi.mock('@/stores/workflows/subblock/store', () => ({ + useSubBlockStore: create<{ + workflowValues: Record>> + getValue: (block: string, field: string) => unknown + setValue: (block: string, field: string, value: unknown) => void + }>((set, get) => ({ + workflowValues: {}, + getValue: (block, field) => get().workflowValues['workflow-1']?.[block]?.[field], + setValue: (block, field, value) => + set((state) => ({ + workflowValues: { + 'workflow-1': { + ...state.workflowValues['workflow-1'], + [block]: { ...state.workflowValues['workflow-1']?.[block], [field]: value }, + }, + }, + })), + })), +})) + +/** Keep the real tool-param bridge; substitute only the heavy leaf field renderer. */ +vi.mock( + '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block', + () => ({ + SubBlock: ({ + blockId, + config, + disabled, + }: { + blockId: string + config: SubBlockConfig + disabled: boolean + }) => { + const value = useSubBlockStore((state) => state.getValue(blockId, config.id)) + return ( + + useSubBlockStore.getState().setValue(blockId, config.id, event.target.value) + } + /> + ) + }, + }) +) + +vi.mock('@sim/emcn', () => ({ + Button: ({ variant: _variant, ...props }: ComponentProps<'button'> & { variant?: string }) => ( + + {groups + ?.flatMap((group) => group.items) + .map((item) => ( + + ))} + + ), + Tooltip: { + Root: ({ children }: { children: ReactNode }) => children, + Trigger: ({ children }: { children: ReactNode }) => children, + Content: () => null, + }, + Popover: ({ children }: { children: ReactNode }) => children, + PopoverTrigger: ({ children }: { children: ReactNode }) => children, + PopoverContent: () => null, + PopoverItem: () => null, +})) + +import { ToolInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input' +import { useSubBlockStore } from '@/stores/workflows/subblock/store' + +let container: HTMLDivElement +let root: Root +const tool = (value: string): StoredTool => ({ + type: 'mcp', + toolId: `mcp-${value}`, + title: value, + isExpanded: true, + params: { query: value, serverId: 'server-1', toolName: value }, + schema: { type: 'object', properties: { query: { type: 'string' } } }, +}) +const buttons = () => [...container.querySelectorAll('button[aria-expanded]')] +const render = (props: Partial> = {}) => + act(() => root.render()) +const click = (element: HTMLElement) => act(() => element.click()) + +beforeEach(() => { + vi.clearAllMocks() + fixture.tools = [tool('First'), tool('Second')] + fixture.target = null + fixture.blocks = [] + container = document.createElement('div') + document.body.append(container) + root = createRoot(container) + globalThis.IS_REACT_ACT_ENVIRONMENT = true +}) +afterEach(() => { + act(() => root.unmount()) + container.remove() +}) + +describe('ToolInput local expansion', () => { + it('ignores persisted expansion and independently toggles without writing workflow data', () => { + render() + expect(buttons().map((button) => button.getAttribute('aria-expanded'))).toEqual([ + 'false', + 'false', + ]) + click(buttons()[0]) + click(buttons()[1]) + click(buttons()[0]) + expect(buttons().map((button) => button.getAttribute('aria-expanded'))).toEqual([ + 'false', + 'true', + ]) + expect(fixture.write).not.toHaveBeenCalled() + expect(fixture.canonical).not.toHaveBeenCalled() + }) + + it('preserves an immediate parameter edit through collapse and real bridge rehydration', () => { + render() + click(buttons()[0]) + const input = container.querySelector('input')! + act(() => { + Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!.call( + input, + 'Edited' + ) + input.dispatchEvent(new Event('input', { bubbles: true })) + }) + expect(fixture.tools[0].params?.query).toBe('Edited') + click(buttons()[0]) + click(buttons()[0]) + expect(container.querySelector('input')?.value).toBe('Edited') + expect(fixture.write).toHaveBeenCalledTimes(1) + }) + + it('retains the correct duplicate instance when an earlier row is removed', () => { + fixture.tools[1].toolId = fixture.tools[0].toolId + render() + click(buttons()[1]) + click(container.querySelector('button[aria-label="Remove tool"]')!) + expect(buttons()[0].getAttribute('aria-expanded')).toBe('true') + expect(container.querySelector('input')?.value).toBe('Second') + }) + + it('follows the dragged instance when rows reorder', () => { + render() + click(buttons()[0]) + const rows = container.querySelectorAll('[draggable="true"]') + const transfer = { setData: () => {}, effectAllowed: '', dropEffect: '' } + act(() => { + const event = new Event('dragstart', { bubbles: true }) + Object.assign(event, { dataTransfer: transfer }) + rows[1].dispatchEvent(event) + }) + act(() => { + const event = new Event('drop', { bubbles: true, cancelable: true }) + Object.assign(event, { dataTransfer: transfer }) + rows[0].dispatchEvent(event) + }) + expect(buttons().map((button) => button.getAttribute('aria-expanded'))).toEqual([ + 'false', + 'true', + ]) + expect(container.querySelector('input')?.value).toBe('First') + }) + + it('resets after an external replacement or a different editor scope', () => { + render() + click(buttons()[0]) + act(() => fixture.replace([tool('Replacement')])) + expect(buttons()[0].getAttribute('aria-expanded')).toBe('false') + click(buttons()[0]) + render({ blockId: 'block-2' }) + expect(buttons()[0].getAttribute('aria-expanded')).toBe('false') + }) + + it('opens search matches without changing the local choice or stored tools', () => { + fixture.target = { subBlockId: 'tools', valuePath: [1, 'params', 'query'] } + render() + expect(buttons().map((button) => button.getAttribute('aria-expanded'))).toEqual([ + 'false', + 'true', + ]) + fixture.target = null + render({ disabled: true }) + expect(buttons()[1].getAttribute('aria-expanded')).toBe('false') + expect(fixture.write).not.toHaveBeenCalled() + }) + + it('permits locked inspection and opt-in preview expansion without editable fields', () => { + render({ disabled: true }) + click(buttons()[0]) + expect(container.querySelector('input')?.disabled).toBe(true) + render({ isPreview: true, previewValue: fixture.tools }) + expect(buttons()[0].disabled).toBe(true) + render({ isPreview: true, previewValue: fixture.tools, allowExpandInPreview: true }) + click(buttons()[0]) + expect(container.querySelector('input')?.disabled).toBe(true) + expect(fixture.write).not.toHaveBeenCalled() + }) + + it('opens only the newly added configurable tool without persisting expansion flags', () => { + render() + click(buttons()[0]) + click( + [...container.querySelectorAll('button')].find( + (button) => button.textContent === 'Open tools' + )! + ) + click( + [...container.querySelectorAll('button')].find( + (button) => button.textContent === 'Add MCP Server (Advanced)' + )! + ) + expect(buttons().map((button) => button.getAttribute('aria-expanded'))).toEqual([ + 'true', + 'false', + 'true', + ]) + expect(fixture.tools[2]).not.toHaveProperty('isExpanded') + expect(fixture.tools[0].isExpanded).toBe(true) + }) +}) 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 b5e2bb9aed2..abdbeb8d256 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 @@ -2,6 +2,7 @@ import type React from 'react' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Badge, + Button, Combobox, type ComboboxOption, type ComboboxOptionGroup, @@ -14,6 +15,7 @@ import { } from '@sim/emcn' import { ArrowLeft, ChevronRight, Server, Wrench, X } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' +import { isEqual } from 'es-toolkit' import { useParams } from 'next/navigation' import { McpIcon, WorkflowIcon } from '@/components/icons' import { getManagedMcpConnectorIcon } from '@/lib/credential-groups/managed-mcp-connector-icons' @@ -374,7 +376,25 @@ export const ToolInput = memo(function ToolInput({ const workspaceId = params.workspaceId as string const workflowId = params.workflowId as string const activeSearchTarget = useActiveSearchTarget() - const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) + const [storeValue, persistTools] = useSubBlockValue(blockId, subBlockId) + const value = isPreview ? previewValue : storeValue + const scope = `${workflowId}/${blockId}/${subBlockId}/${isPreview}` + const [expansionSource, setExpansionSource] = useState({ scope, value }) + const [localExpanded, setLocalExpanded] = useState>({}) + + /** External replacements have no stable instance IDs, so reset rather than attach an open row to another tool. */ + if (expansionSource.scope !== scope || !isEqual(expansionSource.value, value)) { + setExpansionSource({ scope, value }) + setLocalExpanded({}) + } + + const setStoreValue = useCallback( + (tools: StoredTool[]) => { + setExpansionSource({ scope, value: tools }) + persistTools(tools) + }, + [scope, persistTools] + ) const [open, setOpen] = useState(false) const [customToolModalOpen, setCustomToolModalOpen] = useState(false) const [mcpModalOpen, setMcpModalOpen] = useState(false) @@ -397,12 +417,15 @@ export const ToolInput = memo(function ToolInput({ (oldTools: StoredTool[], newTools: StoredTool[]) => { const next = reindexToolCanonicalModes(oldTools, newTools, canonicalModeOverrides) if (next) collaborativeSetBlockCanonicalModes(blockId, next) + setLocalExpanded((expanded) => + Object.fromEntries( + newTools.map((tool, index) => [index, expanded[oldTools.indexOf(tool)] ?? false]) + ) + ) }, [canonicalModeOverrides, collaborativeSetBlockCanonicalModes, blockId] ) - const value = isPreview ? previewValue : storeValue - const selectedTools: StoredTool[] = Array.isArray(value) && value.length > 0 && @@ -757,12 +780,12 @@ export const ToolInput = memo(function ToolInput({ title: toolBlock.name, toolId: toolId, params: initialParams, - isExpanded: true, operation: defaultOperation, usageControl: 'auto', } - setStoreValue([...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), newTool]) + setLocalExpanded((expanded) => ({ ...expanded, [selectedTools.length]: true })) + setStoreValue([...selectedTools, newTool]) setOpen(false) }, @@ -780,20 +803,18 @@ export const ToolInput = memo(function ToolInput({ type: 'custom-tool', customToolId: customTool.id, usageControl: 'auto', - isExpanded: true, } : { type: 'custom-tool', title: customTool.title, toolId: `custom-${customTool.schema?.function?.name || 'unknown'}`, params: {}, - isExpanded: true, schema: customTool.schema, code: customTool.code || '', usageControl: 'auto', } - setStoreValue([...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), newTool]) + setStoreValue([...selectedTools, newTool]) }, [isPreview, disabled, selectedTools, setStoreValue] ) @@ -832,7 +853,6 @@ export const ToolInput = memo(function ToolInput({ type: 'custom-tool', customToolId: customTool.id, usageControl: existingTool.usageControl || 'auto', - isExpanded: existingTool.isExpanded, } : { ...existingTool, @@ -1003,24 +1023,9 @@ export const ToolInput = memo(function ToolInput({ [isPreview, disabled, selectedTools, setStoreValue] ) - const [localExpanded, setLocalExpanded] = useState>({}) - const toggleToolExpansion = (toolIndex: number) => { if (isPreview && !allowExpandInPreview) return - - if (isPreview || disabled) { - setLocalExpanded((prev) => ({ - ...prev, - [toolIndex]: !(prev[toolIndex] ?? !!selectedTools[toolIndex]?.isExpanded), - })) - return - } - - setStoreValue( - selectedTools.map((tool, index) => - index === toolIndex ? { ...tool, isExpanded: !tool.isExpanded } : tool - ) - ) + setLocalExpanded((expanded) => ({ ...expanded, [toolIndex]: !expanded[toolIndex] })) } const handleDragStart = (e: React.DragEvent, index: number) => { @@ -1044,13 +1049,8 @@ export const ToolInput = memo(function ToolInput({ const handleMcpToolSelect = useCallback( (newTool: StoredTool, closePopover = true) => { - setStoreValue([ - ...selectedTools.map((tool) => ({ - ...tool, - isExpanded: false, - })), - newTool, - ]) + setLocalExpanded((expanded) => ({ ...expanded, [selectedTools.length]: true })) + setStoreValue([...selectedTools, newTool]) if (closePopover) { setMcpServerDrilldown(null) @@ -1168,13 +1168,9 @@ export const ToolInput = memo(function ToolInput({ const serverBinding: StoredTool = { type: MCP_SERVER_ADVANCED_TOOL_TYPE, params: { serverId: mcpServerDrilldown }, - isExpanded: false, usageControl: 'auto', } - const nextTools = [ - ...filteredTools.map((tool) => ({ ...tool, isExpanded: false })), - serverBinding, - ] + const nextTools = [...filteredTools, serverBinding] reindexCanonicalModesOnMutate(selectedTools, filteredTools) setStoreValue(nextTools) setMcpServerDrilldown(null) @@ -1203,7 +1199,6 @@ export const ToolInput = memo(function ToolInput({ toolName: mcpTool.name, serverName: mcpTool.serverName, }, - isExpanded: true, usageControl: 'auto', schema: { ...mcpTool.inputSchema, @@ -1275,12 +1270,8 @@ export const ToolInput = memo(function ToolInput({ type: 'custom-tool', customToolId: customTool.id, usageControl: 'auto', - isExpanded: true, } - setStoreValue([ - ...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), - newTool, - ]) + setStoreValue([...selectedTools, newTool]) setOpen(false) }, } @@ -1376,13 +1367,10 @@ export const ToolInput = memo(function ToolInput({ params: { workflowId: workflow.id, }, - isExpanded: true, usageControl: 'auto', } - setStoreValue([ - ...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), - newTool, - ]) + setLocalExpanded((expanded) => ({ ...expanded, [selectedTools.length]: true })) + setStoreValue([...selectedTools, newTool]) setOpen(false) }, disabled: isPreview || disabled || alreadySelected, @@ -1400,12 +1388,12 @@ export const ToolInput = memo(function ToolInput({ value: 'action-mcp-server-advanced', icon: Server, onSelect: () => { + setLocalExpanded((expanded) => ({ ...expanded, [selectedTools.length]: true })) setStoreValue([ - ...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), + ...selectedTools, { type: MCP_SERVER_ADVANCED_TOOL_TYPE, params: { serverId: '' }, - isExpanded: true, usageControl: 'auto', }, ]) @@ -1556,11 +1544,9 @@ export const ToolInput = memo(function ToolInput({ activeSearchTarget?.subBlockId === subBlockId && activeSearchTarget.valuePath[0] === toolIndex && activeSearchTarget.valuePath[1] === 'params' - const isExpandedForDisplay = hasToolBody - ? isPreview || disabled - ? isSearchExpanded || (localExpanded[toolIndex] ?? !!tool.isExpanded) - : isSearchExpanded || !!tool.isExpanded - : false + const isExpandedForDisplay = + hasToolBody && (isSearchExpanded || !!localExpanded[toolIndex]) + const bodyId = `${blockId}-${subBlockId}-tool-${toolIndex}` return (
handleDragOver(e, toolIndex)} onDrop={(e) => handleDrop(e, toolIndex)} > -
{ - if (isCustomTool) { - handleEditCustomTool(toolIndex) - } else if (hasToolBody) { - toggleToolExpansion(toolIndex) - } - }} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - if (isCustomTool) { - handleEditCustomTool(toolIndex) - } else if (hasToolBody) { - toggleToolExpansion(toolIndex) - } - } - }} - > +
-
{ + if (isCustomTool) { + handleEditCustomTool(toolIndex) + } else if (hasToolBody) { + toggleToolExpansion(toolIndex) + } }} > - {isCustomTool ? ( - - ) : isMcpFamily ? ( - - ) : isWorkflowTool ? ( - - ) : ( - - )} -
- - {formatDisplayText(toolDisplayName ?? '', { - workflowSearchHighlight: getToolTitleSearchHighlight(toolIndex), - })} - +
+ {isCustomTool ? ( + + ) : isMcpFamily ? ( + + ) : isWorkflowTool ? ( + + ) : ( + + )} +
+ + {formatDisplayText(toolDisplayName ?? '', { + workflowSearchHighlight: getToolTitleSearchHighlight(toolIndex), + })} + + {isMcpTool && !mcpDataLoading && (() => { @@ -1804,7 +1788,10 @@ export const ToolInput = memo(function ToolInput({
{!isCustomTool && isExpandedForDisplay && ( -
+
{/* Operation dropdown for tools with multiple operations */} {(() => { if (!hasOperations) return null @@ -1830,7 +1817,7 @@ export const ToolInput = memo(function ToolInput({ placeholder='Select operation' /* Denied operations only drop out once the config resolves, and picking one rewrites the stored tool. */ - disabled={disabled || isPermissionLoading} + disabled={isPreview || disabled || isPermissionLoading} />
) @@ -1857,7 +1844,9 @@ export const ToolInput = memo(function ToolInput({ hasCanonicalPair && canonicalMode && canonicalId ? { mode: canonicalMode, + disabled: isPreview || disabled, onToggle: () => { + if (isPreview || disabled) return const nextMode = canonicalMode === 'advanced' ? 'basic' : 'advanced' collaborativeSetBlockCanonicalMode( blockId, @@ -1886,7 +1875,7 @@ export const ToolInput = memo(function ToolInput({ toolType={tool.type} toolParams={tool.params} onParamChange={handleParamChange} - disabled={disabled} + disabled={isPreview || disabled} canonicalToggle={canonicalToggleProp} />