Skip to content

Commit aad2544

Browse files
fix(copilot): don't surface custom blocks whose definition was deleted (#5498)
* fix(copilot): keep custom blocks out of the VFS static component cache so a deleted definition doesn't linger * fix(copilot): drop deleted-definition custom blocks from a workflow's state so the copilot can't see unrenderable blocks * fix(copilot): don't strip placed custom blocks when the definition load fails (null vs empty set) * refactor(copilot): drop unreachable static-cache custom-block filter The VFS static cache is only ever built by materialize(), which runs outside any custom-block overlay (the overlay is scoped to edit_workflow / get_blocks_metadata, neither of which materializes the VFS). So getAllBlocks() there always returns first-party blocks only and no custom block can be frozen into the cache. Removed the dead guard.
1 parent 858f7d2 commit aad2544

1 file changed

Lines changed: 51 additions & 2 deletions

File tree

apps/sim/lib/copilot/vfs/workspace-vfs.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ import {
123123
hasWorkspaceAdminAccess,
124124
} from '@/lib/workspaces/permissions/utils'
125125
import { computeNeedsRedeployment } from '@/app/api/workflows/utils'
126-
import { buildCustomBlockConfig } from '@/blocks/custom/build-config'
126+
import { buildCustomBlockConfig, isCustomBlockType } from '@/blocks/custom/build-config'
127127
import { getAllBlocks } from '@/blocks/registry'
128128
import type { BlockIcon } from '@/blocks/types'
129129
import { CONNECTOR_REGISTRY } from '@/connectors/registry.server'
@@ -405,6 +405,18 @@ export class WorkspaceVFS {
405405
private deploymentCache = new Map<string, Promise<DeploymentData | null>>()
406406
private _workspaceId = ''
407407
private _betaEnabled = false
408+
/**
409+
* Types of the org's CURRENT custom blocks (enabled + disabled — a disabled block
410+
* still resolves/renders). Populated by {@link materializeCustomBlocks}; used to
411+
* drop a placed custom block from a workflow's state when its definition has been
412+
* deleted, so the copilot never sees a block it can't render.
413+
*
414+
* `null` means "not loaded" — either not materialized yet or the load FAILED. In
415+
* that case {@link dropDeletedCustomBlocks} strips nothing, so a transient failure
416+
* can't wrongly nuke every placed custom block. An empty `Set` is distinct: it
417+
* means the org genuinely has no custom blocks, so any placed one IS deleted.
418+
*/
419+
private _customBlockTypes: Set<string> | null = null
408420

409421
get workspaceId(): string {
410422
return this._workspaceId
@@ -425,12 +437,45 @@ export class WorkspaceVFS {
425437
): Promise<Awaited<ReturnType<typeof loadWorkflowFromNormalizedTables>>> {
426438
let cached = this.normalizedCache.get(workflowId)
427439
if (!cached) {
428-
cached = loadWorkflowFromNormalizedTables(workflowId)
440+
cached = loadWorkflowFromNormalizedTables(workflowId).then((n) =>
441+
this.dropDeletedCustomBlocks(n)
442+
)
429443
this.normalizedCache.set(workflowId, cached)
430444
}
431445
return cached
432446
}
433447

448+
/**
449+
* Strip placed custom blocks whose definition no longer exists from a loaded
450+
* workflow (and any edges touching them), so the copilot never sees a block it
451+
* can't render — mirroring how the serializer drops an unresolvable custom block.
452+
* A live definition (enabled or disabled) is kept; only a DELETED one is removed.
453+
* Runs lazily (after materialize), so `_customBlockTypes` is populated by then.
454+
*/
455+
private dropDeletedCustomBlocks(
456+
normalized: Awaited<ReturnType<typeof loadWorkflowFromNormalizedTables>>
457+
): Awaited<ReturnType<typeof loadWorkflowFromNormalizedTables>> {
458+
// `null` = definitions never loaded (or the load failed) — strip nothing rather
459+
// than treat every placed custom block as deleted.
460+
if (!normalized || this._customBlockTypes === null) return normalized
461+
const validTypes = this._customBlockTypes
462+
const dropped = new Set<string>()
463+
const blocks: Record<string, unknown> = {}
464+
for (const [id, block] of Object.entries(normalized.blocks)) {
465+
const type = (block as { type?: string }).type
466+
if (isCustomBlockType(type) && !validTypes.has(type)) {
467+
dropped.add(id)
468+
continue
469+
}
470+
blocks[id] = block
471+
}
472+
if (dropped.size === 0) return normalized
473+
const edges = (normalized.edges ?? []).filter(
474+
(e) => !dropped.has(e.source) && !dropped.has(e.target)
475+
)
476+
return { ...normalized, blocks: blocks as typeof normalized.blocks, edges }
477+
}
478+
434479
/** Load a workflow's deployment data once per instance (deployment.json + versions.json share it). */
435480
private loadDeployments(wf: {
436481
id: string
@@ -523,6 +568,7 @@ export class WorkspaceVFS {
523568
this.lazy = new Map()
524569
this.normalizedCache = new Map()
525570
this.deploymentCache = new Map()
571+
this._customBlockTypes = null
526572
this._workspaceId = workspaceId
527573
this._betaEnabled = await isFeatureEnabled('mothership-beta', { userId })
528574

@@ -1824,6 +1870,9 @@ export class WorkspaceVFS {
18241870
): Promise<NonNullable<WorkspaceMdData['customBlocks']>> {
18251871
try {
18261872
const blocks = await listCustomBlocksWithInputsForWorkspace(workspaceId)
1873+
// Every current definition (incl. disabled) — the authoritative set used to
1874+
// drop deleted-definition instances from workflow state (see loadNormalized).
1875+
this._customBlockTypes = new Set(blocks.map((cb) => cb.type))
18271876
const summary: NonNullable<WorkspaceMdData['customBlocks']> = []
18281877

18291878
for (const cb of blocks) {

0 commit comments

Comments
 (0)