From 5db27673ff926b04fc1708734458dc347008eeb1 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 23 Aug 2026 11:51:26 -0700 Subject: [PATCH 1/3] refactor: replace hand-rolled utilities and dead code with the shared forms Each of these has a mandated helper or an established accessor in the repo that the site predates or missed. All are behavior-preserving: - `omit()` for the three `Object.fromEntries(Object.entries(x).filter(...))` block-input filters, which also recovers the `Omit` typing that `Object.fromEntries` erases to an index signature. - `getErrorMessage()` for the inline `instanceof Error` message ternary. - `getBlock()` for two `getAllBlocks().find((b) => b.type === x)` scans, one of them inside a loop over selected tools. The same file already resolves the same values through `getBlock`. - A memoised `Map` for three `.find()`-by-id scans over the workspace skill list, one of them inside a render `.map()`. - `SELECTOR_SEARCH_STALE` for three copy-pasted `15 * 1000` literals. They are deliberately shorter than `SELECTOR_STALE`, so this is a new named constant rather than a fold into the existing one. - Tailwind classes for the static half of two duplicated anchor styles, keeping only the genuinely dynamic `left`/`top` inline. - Dropped the unused `catch` bindings on three intentional JSON-parse swallows. `panel.tsx`'s run-button gate loses a `TODO`-stubbed `hasValidationErrors = false` and the `isWorkflowBlocked` term built on it. That term was dead twice over: it reduced to `isExecuting`, and the enclosing expression is already guarded by `!isExecuting`. --- .../plus-menu-dropdown/plus-menu-dropdown.tsx | 10 ++-------- .../skills-menu-dropdown/skills-menu-dropdown.tsx | 10 ++-------- .../components/skill-input/skill-input.tsx | 13 +++++++++---- .../sub-block/components/tool-input/tool-input.tsx | 5 ++--- .../w/[workflowId]/components/panel/panel.tsx | 7 ++----- apps/sim/blocks/blocks/fireflies.ts | 5 ++--- apps/sim/blocks/blocks/grain.ts | 3 ++- apps/sim/blocks/blocks/stt.ts | 5 ++--- apps/sim/executor/handlers/api/api-handler.ts | 2 +- apps/sim/executor/utils.ts | 4 ++-- apps/sim/executor/utils/errors.ts | 4 ++-- .../hooks/selectors/providers/google/selectors.ts | 8 ++++++-- .../sim/hooks/selectors/providers/jira/selectors.ts | 9 +++++++-- apps/sim/hooks/selectors/providers/shared.ts | 9 +++++++++ .../hooks/selectors/providers/webflow/selectors.ts | 8 ++++++-- 15 files changed, 56 insertions(+), 46 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown.tsx index 1bc821bd712..da971050c25 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown.tsx @@ -289,14 +289,8 @@ export const PlusMenuDropdown = React.memo(
(null) const [editingSkillSnapshot, setEditingSkillSnapshot] = useState(null) + const skillsById = useMemo( + () => new Map(workspaceSkills.map((skill) => [skill.id, skill])), + [workspaceSkills] + ) + // Prefer the live query cache so the modal reflects concurrent edits, but // fall back to the click-time snapshot when a background refetch drops the // skill — otherwise the modal would close mid-edit and silently discard the // draft; saving surfaces the real server error instead. const editingSkill = editingSkillId - ? (workspaceSkills.find((s) => s.id === editingSkillId) ?? editingSkillSnapshot) + ? (skillsById.get(editingSkillId) ?? editingSkillSnapshot) : null const selectedSkills: StoredSkill[] = useMemo(() => { @@ -119,10 +124,10 @@ export function SkillInput({ const resolveSkillName = useCallback( (stored: StoredSkill): string => { - const found = workspaceSkills.find((s) => s.id === stored.skillId) + const found = skillsById.get(stored.skillId) return found?.name ?? stored.name ?? stored.skillId }, - [workspaceSkills] + [skillsById] ) return ( @@ -141,7 +146,7 @@ export function SkillInput({ {selectedSkills.length > 0 && selectedSkills.map((stored, index) => { - const fullSkill = workspaceSkills.find((s) => s.id === stored.skillId) + const fullSkill = skillsById.get(stored.skillId) const skillName = resolveSkillName(stored) const workflowSearchHighlight = getWorkflowSearchLabelHighlight({ activeSearchTarget, 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 9b28cfb9797..2c222bdfbef 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 @@ -520,7 +520,7 @@ export const ToolInput = memo(function ToolInput({ // subBlock): shown in the picker but greyed out with a tooltip instead of added. const blockType = useWorkflowStore(useCallback((state) => state.blocks[blockId]?.type, [blockId])) const unsupportedToolTypes = useMemo(() => { - const block = getAllBlocks().find((b) => b.type === blockType) + const block = getBlock(blockType) return block?.subBlocks.find((sb) => sb.id === subBlockId)?.unsupportedToolTypes ?? [] }, [blockType, subBlockId]) const mcpUnsupported = unsupportedToolTypes.includes('mcp') @@ -529,9 +529,8 @@ export const ToolInput = memo(function ToolInput({ // Look up credential type for reactive condition filtering (e.g. service account detection). // Uses canonical resolution so the active field (basic vs advanced) is respected. const toolCredentialId = useMemo(() => { - const allBlocks = getAllBlocks() for (const [toolIndex, tool] of selectedTools.entries()) { - const blockConfig = allBlocks.find((b: { type: string }) => b.type === tool.type) + const blockConfig = getBlock(tool.type) if (!blockConfig?.subBlocks) continue // canonical-index-unscoped: a nested tool resolves against `tool.params`, which only ever // holds action-surface values — a tool is never invoked in trigger mode. diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 6069a89780c..035d9dfef2f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -640,13 +640,10 @@ export const Panel = memo(function Panel() { setIsMenuOpen(false) }, [collaborativeBatchToggleLocked]) - // Compute run button state - const canRun = userPermissions.canRead // Running only requires read permissions + const canRun = userPermissions.canRead const isLoadingPermissions = userPermissions.isLoading - const hasValidationErrors = false // TODO: Add validation logic if needed - const isWorkflowBlocked = isExecuting || hasValidationErrors const isButtonDisabled = - !isExecuting && (isUsageGateLoading || isWorkflowBlocked || (!canRun && !isLoadingPermissions)) + !isExecuting && (isUsageGateLoading || (!canRun && !isLoadingPermissions)) /** * Register global keyboard shortcuts using the central commands registry. diff --git a/apps/sim/blocks/blocks/fireflies.ts b/apps/sim/blocks/blocks/fireflies.ts index 86320cd4654..b9bc472650b 100644 --- a/apps/sim/blocks/blocks/fireflies.ts +++ b/apps/sim/blocks/blocks/fireflies.ts @@ -1,3 +1,4 @@ +import { omit } from '@sim/utils/object' import { FirefliesIcon } from '@/components/icons' import { resolveHttpsUrlFromFileInput } from '@/lib/uploads/utils/file-utils' import type { BlockConfig, BlockMeta } from '@/blocks/types' @@ -698,9 +699,7 @@ Return ONLY the summary text - no quotes, no labels.`, const firefliesV2SubBlocks = (FirefliesBlock.subBlocks || []).filter( (subBlock) => subBlock.id !== 'audioUrl' ) -const firefliesV2Inputs = FirefliesBlock.inputs - ? Object.fromEntries(Object.entries(FirefliesBlock.inputs).filter(([key]) => key !== 'audioUrl')) - : {} +const firefliesV2Inputs = FirefliesBlock.inputs ? omit(FirefliesBlock.inputs, ['audioUrl']) : {} export const FirefliesV2Block: BlockConfig = { ...FirefliesBlock, diff --git a/apps/sim/blocks/blocks/grain.ts b/apps/sim/blocks/blocks/grain.ts index d8fb3eae33f..c7ef78614d9 100644 --- a/apps/sim/blocks/blocks/grain.ts +++ b/apps/sim/blocks/blocks/grain.ts @@ -1,3 +1,4 @@ +import { omit } from '@sim/utils/object' import { GrainIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' @@ -758,7 +759,7 @@ export const GrainV2Block: BlockConfig = { }, }, inputs: { - ...Object.fromEntries(Object.entries(GrainBlock.inputs).filter(([key]) => key !== 'viewId')), + ...omit(GrainBlock.inputs, ['viewId']), apiKey: { type: 'string', description: 'Grain API key (Personal or Workspace Access Token)' }, hookType: { type: 'string', description: 'Grain event type for the webhook' }, hookInclude: { diff --git a/apps/sim/blocks/blocks/stt.ts b/apps/sim/blocks/blocks/stt.ts index 8b52edd546a..0878ec0bdf2 100644 --- a/apps/sim/blocks/blocks/stt.ts +++ b/apps/sim/blocks/blocks/stt.ts @@ -1,3 +1,4 @@ +import { omit } from '@sim/utils/object' import { STTIcon } from '@/components/icons' import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types' import { createVersionedToolSelector, normalizeFileInput } from '@/blocks/utils' @@ -368,9 +369,7 @@ export const SttBlock: BlockConfig = { }, } -const sttV2Inputs = SttBlock.inputs - ? Object.fromEntries(Object.entries(SttBlock.inputs).filter(([key]) => key !== 'audioUrl')) - : {} +const sttV2Inputs = SttBlock.inputs ? omit(SttBlock.inputs, ['audioUrl']) : {} const sttV2SubBlocks = (SttBlock.subBlocks || []).filter((subBlock) => subBlock.id !== 'audioUrl') export const SttV2Block: BlockConfig = { diff --git a/apps/sim/executor/handlers/api/api-handler.ts b/apps/sim/executor/handlers/api/api-handler.ts index b6a28517d7d..8595ef00a4b 100644 --- a/apps/sim/executor/handlers/api/api-handler.ts +++ b/apps/sim/executor/handlers/api/api-handler.ts @@ -58,7 +58,7 @@ export class ApiBlockHandler implements BlockHandler { if (trimmedBody.startsWith('{') || trimmedBody.startsWith('[')) { processedInputs.body = JSON.parse(trimmedBody) } - } catch (e) {} + } catch {} } else if (processedInputs.body === null) { processedInputs.body = undefined } diff --git a/apps/sim/executor/utils.ts b/apps/sim/executor/utils.ts index 782f6e9f8f8..c509e4121ca 100644 --- a/apps/sim/executor/utils.ts +++ b/apps/sim/executor/utils.ts @@ -113,7 +113,7 @@ export class StreamingResponseFormatProcessor implements ResponseFormatStreamPro return null } - } catch (e) {} + } catch {} const openBraces = (buffer.match(/\{/g) || []).length const closeBraces = (buffer.match(/\}/g) || []).length @@ -138,7 +138,7 @@ export class StreamingResponseFormatProcessor implements ResponseFormatStreamPro return null } - } catch (e) {} + } catch {} } return null diff --git a/apps/sim/executor/utils/errors.ts b/apps/sim/executor/utils/errors.ts index 7bbef22da0e..ba0d35d0ff1 100644 --- a/apps/sim/executor/utils/errors.ts +++ b/apps/sim/executor/utils/errors.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from '@sim/utils/errors' import { HttpError } from '@/lib/core/utils/http-error' import type { ExecutionContext, ExecutionResult } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' @@ -49,8 +50,7 @@ export interface BlockExecutionErrorDetails { * every block boundary. */ export function buildBlockExecutionError(details: BlockExecutionErrorDetails): Error { - const errorMessage = - details.error instanceof Error ? details.error.message : String(details.error) + const errorMessage = getErrorMessage(details.error) const blockName = details.block.metadata?.name || details.block.id const blockType = details.block.metadata?.id || 'unknown' diff --git a/apps/sim/hooks/selectors/providers/google/selectors.ts b/apps/sim/hooks/selectors/providers/google/selectors.ts index 1f5a347e03b..6ad9c5540dc 100644 --- a/apps/sim/hooks/selectors/providers/google/selectors.ts +++ b/apps/sim/hooks/selectors/providers/google/selectors.ts @@ -1,6 +1,10 @@ import { requestJson } from '@/lib/api/client/request' import * as selectorContracts from '@/lib/api/contracts/selectors' -import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared' +import { + ensureCredential, + SELECTOR_SEARCH_STALE, + SELECTOR_STALE, +} from '@/hooks/selectors/providers/shared' import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types' export const googleSelectors = { @@ -101,7 +105,7 @@ export const googleSelectors = { selectorContracts.googleDriveFilesSelectorContract, selectorContracts.googleDriveFileSelectorContract, ], - staleTime: 15 * 1000, + staleTime: SELECTOR_SEARCH_STALE, getQueryKey: ({ context, search }: SelectorQueryArgs) => [ 'selectors', 'google.drive', diff --git a/apps/sim/hooks/selectors/providers/jira/selectors.ts b/apps/sim/hooks/selectors/providers/jira/selectors.ts index 6175f8a0961..f8743641b50 100644 --- a/apps/sim/hooks/selectors/providers/jira/selectors.ts +++ b/apps/sim/hooks/selectors/providers/jira/selectors.ts @@ -1,7 +1,12 @@ import { requestJson } from '@/lib/api/client/request' import * as selectorContracts from '@/lib/api/contracts/selectors' import { fetchOAuthToken } from '@/hooks/selectors/helpers' -import { ensureCredential, ensureDomain, SELECTOR_STALE } from '@/hooks/selectors/providers/shared' +import { + ensureCredential, + ensureDomain, + SELECTOR_SEARCH_STALE, + SELECTOR_STALE, +} from '@/hooks/selectors/providers/shared' import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types' export const jiraSelectors = { @@ -71,7 +76,7 @@ export const jiraSelectors = { selectorContracts.jiraIssuesSelectorContract, selectorContracts.jiraIssueSelectorContract, ], - staleTime: 15 * 1000, + staleTime: SELECTOR_SEARCH_STALE, getQueryKey: ({ context, search }: SelectorQueryArgs) => [ 'selectors', 'jira.issues', diff --git a/apps/sim/hooks/selectors/providers/shared.ts b/apps/sim/hooks/selectors/providers/shared.ts index 2cc13702cf2..927113feaa6 100644 --- a/apps/sim/hooks/selectors/providers/shared.ts +++ b/apps/sim/hooks/selectors/providers/shared.ts @@ -2,6 +2,15 @@ import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types' export const SELECTOR_STALE = 60 * 1000 +/** + * Stale window for selectors whose result set is search-backed. + * + * Shorter than {@link SELECTOR_STALE} because the query key carries the search + * term, so a stale entry is a stale answer to a question the user is still + * typing rather than a stale copy of a stable list. + */ +export const SELECTOR_SEARCH_STALE = 15 * 1000 + export const ensureCredential = (context: SelectorContext, key: SelectorKey): string => { if (!context.oauthCredential) { throw new Error(`Missing credential for selector ${key}`) diff --git a/apps/sim/hooks/selectors/providers/webflow/selectors.ts b/apps/sim/hooks/selectors/providers/webflow/selectors.ts index f1f0c9e58d0..65eeec52304 100644 --- a/apps/sim/hooks/selectors/providers/webflow/selectors.ts +++ b/apps/sim/hooks/selectors/providers/webflow/selectors.ts @@ -1,6 +1,10 @@ import { requestJson } from '@/lib/api/client/request' import * as selectorContracts from '@/lib/api/contracts/selectors' -import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared' +import { + ensureCredential, + SELECTOR_SEARCH_STALE, + SELECTOR_STALE, +} from '@/hooks/selectors/providers/shared' import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types' export const webflowSelectors = { @@ -59,7 +63,7 @@ export const webflowSelectors = { 'webflow.items': { key: 'webflow.items', contracts: [selectorContracts.webflowItemsSelectorContract], - staleTime: 15 * 1000, + staleTime: SELECTOR_SEARCH_STALE, getQueryKey: ({ context, search }: SelectorQueryArgs) => [ 'selectors', 'webflow.items', From d7da73d96d96011dd0557f3527fa7b7c01632d00 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 23 Aug 2026 11:56:40 -0700 Subject: [PATCH 2/3] fix: guard the registry lookups, and scope the search-stale doc to its callers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `getBlock` normalizes its argument with `type.replace(...)`, so it throws on `undefined` where the `getAllBlocks().find(...)` it replaced returned `undefined` harmlessly. Both call sites can be reached without a type: `tool-input` reads `state.blocks[blockId]?.type`, which is undefined once the block is deleted while the panel is mounted — and `Record` indexing hides that from the compiler, so it would have thrown during render. `agent-handler`'s `tool.type` is optional and the compiler did catch it. Also index the skill lookup in `resolveSkillsLabel`, which runs a `.find()` inside a `.map()` for every block on the canvas — the case the memoised map in `skill-input` addressed for one component while leaving the hot path. `providers/utils.ts` keeps its `getAllBlocks().find(...)`: it takes the registry as an injected dependency precisely so a client-reachable module never imports it, and reaching for `getBlock` there would cross that boundary. The new constant's doc claimed search-backed selectors take a shorter window. Several still sit on `SELECTOR_STALE`, so it now describes the value its three callers share rather than asserting a rule the tree does not follow. --- .../sub-block/components/tool-input/tool-input.tsx | 2 +- apps/sim/executor/handlers/agent/agent-handler.ts | 4 ++-- apps/sim/hooks/selectors/providers/shared.ts | 10 ++++++---- apps/sim/lib/workflows/subblocks/display.ts | 4 +++- 4 files changed, 12 insertions(+), 8 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 2c222bdfbef..6bae0deb74f 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 @@ -520,7 +520,7 @@ export const ToolInput = memo(function ToolInput({ // subBlock): shown in the picker but greyed out with a tooltip instead of added. const blockType = useWorkflowStore(useCallback((state) => state.blocks[blockId]?.type, [blockId])) const unsupportedToolTypes = useMemo(() => { - const block = getBlock(blockType) + const block = blockType ? getBlock(blockType) : undefined return block?.subBlocks.find((sb) => sb.id === subBlockId)?.unsupportedToolTypes ?? [] }, [blockType, subBlockId]) const mcpUnsupported = unsupportedToolTypes.includes('mcp') diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index eb0b38e2603..35bfeb4ef15 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -32,7 +32,7 @@ import { selectModelBoundFileInputPaths } from '@/lib/uploads/utils/model-input' import { hydrateUserFilesWithBase64 } from '@/lib/uploads/utils/user-file-base64.server' import { resolveCustomBlockToolBinding } from '@/lib/workflows/custom-blocks/operations' import { getCustomToolById } from '@/lib/workflows/custom-tools/operations' -import { getAllBlocks } from '@/blocks' +import { getAllBlocks, getBlock } from '@/blocks' import { assembleCustomBlockInputMapping, isCustomBlockType } from '@/blocks/custom/build-config' import type { BlockOutput } from '@/blocks/types' import { normalizeFileInput } from '@/blocks/utils' @@ -857,7 +857,7 @@ export class AgentBlockHandler implements BlockHandler { ) if (tool.type === 'mcp' || tool.type === 'custom-tool') return alignedParams - const blockInputs = getAllBlocks().find((block) => block.type === tool.type)?.inputs + const blockInputs = tool.type ? getBlock(tool.type)?.inputs : undefined return prepareResolvedSecretProjectedInputs(alignedParams, blockInputs, formattedParams) } diff --git a/apps/sim/hooks/selectors/providers/shared.ts b/apps/sim/hooks/selectors/providers/shared.ts index 927113feaa6..58d5e3bd90f 100644 --- a/apps/sim/hooks/selectors/providers/shared.ts +++ b/apps/sim/hooks/selectors/providers/shared.ts @@ -3,11 +3,13 @@ import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types' export const SELECTOR_STALE = 60 * 1000 /** - * Stale window for selectors whose result set is search-backed. + * The shorter stale window carried by `google.drive`, `jira.issues` and + * `webflow.items`, whose listings turn over faster than {@link SELECTOR_STALE} + * assumes. * - * Shorter than {@link SELECTOR_STALE} because the query key carries the search - * term, so a stale entry is a stale answer to a question the user is still - * typing rather than a stale copy of a stable list. + * Not every search-backed selector uses it — several still sit on + * {@link SELECTOR_STALE} — so treat this as the value those three share rather + * than a rule about search. */ export const SELECTOR_SEARCH_STALE = 15 * 1000 diff --git a/apps/sim/lib/workflows/subblocks/display.ts b/apps/sim/lib/workflows/subblocks/display.ts index fd315db7b77..3defd751782 100644 --- a/apps/sim/lib/workflows/subblocks/display.ts +++ b/apps/sim/lib/workflows/subblocks/display.ts @@ -556,13 +556,15 @@ export function resolveSkillsLabel( if (subBlock?.type !== 'skill-input') return null if (!Array.isArray(rawValue) || rawValue.length === 0) return null + const skillsById = new Map(skills.map((skill) => [skill.id, skill])) + const names = rawValue .map((skill: unknown) => { if (!skill || typeof skill !== 'object') return null const s = skill as { skillId?: string; name?: string } if (s.skillId) { - const found = skills.find((candidate) => candidate.id === s.skillId) + const found = skillsById.get(s.skillId) if (found?.name) return found.name } if (typeof s.name === 'string' && s.name) return s.name From af0528307ebbd2fe2f25849083dc16655ffb937b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 23 Aug 2026 12:53:36 -0700 Subject: [PATCH 3/3] fix: guard the second registry lookup in tool-input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `selectedTools` validates only `value[0]?.type` and then casts the whole array, so a persisted workflow whose later rows lost their `type` yields `undefined` here — the cast is what makes the compiler believe otherwise. `getBlock` normalizes with `type.replace`, so that throws during render. --- .../components/sub-block/components/tool-input/tool-input.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6bae0deb74f..0511dbaea84 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 @@ -530,7 +530,7 @@ export const ToolInput = memo(function ToolInput({ // Uses canonical resolution so the active field (basic vs advanced) is respected. const toolCredentialId = useMemo(() => { for (const [toolIndex, tool] of selectedTools.entries()) { - const blockConfig = getBlock(tool.type) + const blockConfig = tool.type ? getBlock(tool.type) : undefined if (!blockConfig?.subBlocks) continue // canonical-index-unscoped: a nested tool resolves against `tool.params`, which only ever // holds action-surface values — a tool is never invoked in trigger mode.