# extendify/3.1.5/src/Agent/workflows/block-selector/tools/block-patching.js

Extendify, version 3.1.5. 87 lines.

- Page: https://pluginprobe.com/plugins/extendify/3.1.5/code/src/Agent/workflows/block-selector/tools/block-patching.js
- Raw: https://pluginprobe.com/plugins/extendify/3.1.5/raw/src/Agent/workflows/block-selector/tools/block-patching.js
- Modified: 2026-08-12T16:23:58+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/extendify/3.1.5/code/src/Agent/workflows/block-selector/tools/block-patching.js#L10-L20`.

```javascript
import { fetchBlockCodeById } from '@agent/lib/block-code';
import { applyBlockPatch } from '@agent/lib/block-patch';
import { buildNewBlock } from '@agent/lib/insertable-blocks';
import { ensureCoreBlocksRegistered } from '@agent/lib/register-blocks';
import { swapBlockImage } from '@agent/lib/replace-image';
import { useQuickEditStore } from '@quick-edit/state/store';
import apiFetch from '@wordpress/api-fetch';

// clear lists attributes to reset — null in a patch means "no change", not "remove".
const buildEdit = async ({ blockId, patch, clear }, source, postId) => {
	const previousContent = await fetchBlockCodeById(blockId, source, postId);
	if (!previousContent) return null;
	const presetSlugs = window.extAgentData?.context?.presetSlugs ?? {};
	const blockCode = applyBlockPatch(
		previousContent,
		patch,
		clear ?? [],
		presetSlugs,
	);
	if (blockCode === previousContent) return null;
	return { op: 'edit', blockId, block: blockCode };
};

const buildAdd = ({ anchorId, position, blockType, patch, clear }) => {
	const presetSlugs = window.extAgentData?.context?.presetSlugs ?? {};
	const block = buildNewBlock(blockType, patch, clear ?? [], presetSlugs);
	if (!block) return null;
	return { op: 'add', anchorId, position, block };
};

// The backend op has no image — the confirm UI attaches it before the tool runs.
const buildReplaceImage = async ({ blockId, image }, source, postId) => {
	if (!image) return null;
	const previousContent = await fetchBlockCodeById(blockId, source, postId);
	if (!previousContent) return null;
	const blockCode = swapBlockImage(previousContent, image);
	if (!blockCode || blockCode === previousContent) return null;
	return { op: 'edit', blockId, block: blockCode };
};

const BUILDERS = {
	edit: buildEdit,
	'replace-image': buildReplaceImage,
	delete: ({ blockId }) => ({ op: 'delete', blockId }),
	move: ({ blockId, targetId, position }) => ({
		op: 'move',
		blockId,
		targetId,
		position,
	}),
	wrap: ({ blockId, container }) => ({ op: 'wrap', blockId, container }),
	add: buildAdd,
};

export default async (input) => {
	await ensureCoreBlocksRegistered();
	const operations = Array.isArray(input?.operations) ? input.operations : [];
	if (!operations.length) return { refused: true, reason: 'no-block' };

	const { agentBlock } = useQuickEditStore.getState();
	// Template parts have a separate id space; refusing beats a silent no-op.
	if (agentBlock?.source?.kind === 'template-part')
		return { refused: true, reason: 'template-part' };

	const { postId } = window.extAgentData?.context ?? {};
	const results = await Promise.all(
		operations.map((operation) =>
			BUILDERS[operation?.op]?.(operation, agentBlock?.source, postId),
		),
	);
	const built = results.filter(Boolean);
	// A no-op build must surface, or the reply claims a change that never happened.
	const dropped = operations
		.filter((operation, index) => operation && !results[index])
		.map(({ blockId }) => ({ blockId, reason: 'no-change' }));
	if (!built.length) return { refused: true, reason: 'no-op' };

	const { applied = [], refused = [] } = await apiFetch({
		path: '/extendify/v1/agent/update-blocks',
		method: 'POST',
		data: { postId, operations: built },
	});
	if (!applied.length) return { refused: true, reason: 'not-applied' };
	// Not `refused`: Agent.jsx reads any truthy `refused` — even [] — as a refusal.
	return { ok: true, applied, refusedOperations: [...refused, ...dropped] };
};

```
