mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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.
This commit is contained in:
@@ -123,7 +123,7 @@ import {
|
||||
hasWorkspaceAdminAccess,
|
||||
} from '@/lib/workspaces/permissions/utils'
|
||||
import { computeNeedsRedeployment } from '@/app/api/workflows/utils'
|
||||
import { buildCustomBlockConfig } from '@/blocks/custom/build-config'
|
||||
import { buildCustomBlockConfig, isCustomBlockType } from '@/blocks/custom/build-config'
|
||||
import { getAllBlocks } from '@/blocks/registry'
|
||||
import type { BlockIcon } from '@/blocks/types'
|
||||
import { CONNECTOR_REGISTRY } from '@/connectors/registry.server'
|
||||
@@ -405,6 +405,18 @@ export class WorkspaceVFS {
|
||||
private deploymentCache = new Map<string, Promise<DeploymentData | null>>()
|
||||
private _workspaceId = ''
|
||||
private _betaEnabled = false
|
||||
/**
|
||||
* Types of the org's CURRENT custom blocks (enabled + disabled — a disabled block
|
||||
* still resolves/renders). Populated by {@link materializeCustomBlocks}; used to
|
||||
* drop a placed custom block from a workflow's state when its definition has been
|
||||
* deleted, so the copilot never sees a block it can't render.
|
||||
*
|
||||
* `null` means "not loaded" — either not materialized yet or the load FAILED. In
|
||||
* that case {@link dropDeletedCustomBlocks} strips nothing, so a transient failure
|
||||
* can't wrongly nuke every placed custom block. An empty `Set` is distinct: it
|
||||
* means the org genuinely has no custom blocks, so any placed one IS deleted.
|
||||
*/
|
||||
private _customBlockTypes: Set<string> | null = null
|
||||
|
||||
get workspaceId(): string {
|
||||
return this._workspaceId
|
||||
@@ -425,12 +437,45 @@ export class WorkspaceVFS {
|
||||
): Promise<Awaited<ReturnType<typeof loadWorkflowFromNormalizedTables>>> {
|
||||
let cached = this.normalizedCache.get(workflowId)
|
||||
if (!cached) {
|
||||
cached = loadWorkflowFromNormalizedTables(workflowId)
|
||||
cached = loadWorkflowFromNormalizedTables(workflowId).then((n) =>
|
||||
this.dropDeletedCustomBlocks(n)
|
||||
)
|
||||
this.normalizedCache.set(workflowId, cached)
|
||||
}
|
||||
return cached
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip placed custom blocks whose definition no longer exists from a loaded
|
||||
* workflow (and any edges touching them), so the copilot never sees a block it
|
||||
* can't render — mirroring how the serializer drops an unresolvable custom block.
|
||||
* A live definition (enabled or disabled) is kept; only a DELETED one is removed.
|
||||
* Runs lazily (after materialize), so `_customBlockTypes` is populated by then.
|
||||
*/
|
||||
private dropDeletedCustomBlocks(
|
||||
normalized: Awaited<ReturnType<typeof loadWorkflowFromNormalizedTables>>
|
||||
): Awaited<ReturnType<typeof loadWorkflowFromNormalizedTables>> {
|
||||
// `null` = definitions never loaded (or the load failed) — strip nothing rather
|
||||
// than treat every placed custom block as deleted.
|
||||
if (!normalized || this._customBlockTypes === null) return normalized
|
||||
const validTypes = this._customBlockTypes
|
||||
const dropped = new Set<string>()
|
||||
const blocks: Record<string, unknown> = {}
|
||||
for (const [id, block] of Object.entries(normalized.blocks)) {
|
||||
const type = (block as { type?: string }).type
|
||||
if (isCustomBlockType(type) && !validTypes.has(type)) {
|
||||
dropped.add(id)
|
||||
continue
|
||||
}
|
||||
blocks[id] = block
|
||||
}
|
||||
if (dropped.size === 0) return normalized
|
||||
const edges = (normalized.edges ?? []).filter(
|
||||
(e) => !dropped.has(e.source) && !dropped.has(e.target)
|
||||
)
|
||||
return { ...normalized, blocks: blocks as typeof normalized.blocks, edges }
|
||||
}
|
||||
|
||||
/** Load a workflow's deployment data once per instance (deployment.json + versions.json share it). */
|
||||
private loadDeployments(wf: {
|
||||
id: string
|
||||
@@ -523,6 +568,7 @@ export class WorkspaceVFS {
|
||||
this.lazy = new Map()
|
||||
this.normalizedCache = new Map()
|
||||
this.deploymentCache = new Map()
|
||||
this._customBlockTypes = null
|
||||
this._workspaceId = workspaceId
|
||||
this._betaEnabled = await isFeatureEnabled('mothership-beta', { userId })
|
||||
|
||||
@@ -1824,6 +1870,9 @@ export class WorkspaceVFS {
|
||||
): Promise<NonNullable<WorkspaceMdData['customBlocks']>> {
|
||||
try {
|
||||
const blocks = await listCustomBlocksWithInputsForWorkspace(workspaceId)
|
||||
// Every current definition (incl. disabled) — the authoritative set used to
|
||||
// drop deleted-definition instances from workflow state (see loadNormalized).
|
||||
this._customBlockTypes = new Set(blocks.map((cb) => cb.type))
|
||||
const summary: NonNullable<WorkspaceMdData['customBlocks']> = []
|
||||
|
||||
for (const cb of blocks) {
|
||||
|
||||
Reference in New Issue
Block a user