Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions apps/sim/lib/copilot/vfs/workspace-vfs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 })

Expand Down Expand Up @@ -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))
Comment thread
cursor[bot] marked this conversation as resolved.
const summary: NonNullable<WorkspaceMdData['customBlocks']> = []

for (const cb of blocks) {
Expand Down
Loading