Files
webstudio/apps/builder/app/shared/sync/builder-patch.ts
Oleg Isonen dde30ffb54 feat: Improve MCP authoring, compact output, and agent onboarding (#5883)
## Summary

This improves how agents discover, author, inspect, and verify Webstudio
projects while reducing unnecessary MCP context usage.

## What changed

- Adds an **Agents** section to Project Settings with ready-to-run setup
commands and client configuration for Claude Code, Codex, Cursor, and VS
Code.
- Makes large list and schema responses compact and paginated by
default, with **verbose** available when complete records or diagnostics
are needed.
- Applies consistent compact output to assets, instances, styles, design
tokens, CLI schemas, and project-session metadata.
- Returns actionable expression warnings for props, resources, and
Collection bindings, including unknown variables and risky render-time
mutation resources.
- Improves Collection expression guidance so agents can bind array items
with the correct scoped variables.
- Reports rendered audit outcomes explicitly as complete, partial,
confirmation-required, or failed.
- Expands runtime output schemas so MCP clients can validate responses
reliably.
- Strengthens the packaged CLI smoke workflow with a realistic
seven-page stdio MCP scenario.

## Architecture

- Consolidates `project-build` from 65 file-oriented entrypoints to 11
architecture-owned surfaces.
- Separates the core model, contracts, runtime authoring, transfer,
state, project sessions, MCP, visual tooling, persistence codecs, and
authenticated server services.
- Isolates Node-only JSX evaluation from browser-safe transfer utilities
and authenticated server modules.
- Keeps persistence codecs independent from authenticated database
services, preventing unrelated module initialization.
- Defines shared instance-path types once and reuses them across runtime
modules.
- Preserves Immer initialization through explicit package side-effect
metadata.
- Adds package tests that prevent a return to source-file entrypoints or
generic exposure of Node-only surfaces.

## Validation

- Full repository checks, tests, typechecks, generated-file checks, and
fixtures
- All 163 Builder test files and 2,141 tests
- All 71 project-build test files and 1,794 tests
- Lint and package-boundary validation
- Full packed CLI release smoke test, including install, preview, agent
setup, seven-page stdio MCP workflow, rendered audit, and structured
commands
2026-07-15 18:11:28 +02:00

127 lines
3.5 KiB
TypeScript

import type { Change } from "immerhin";
import { produceWithPatches, type Patch } from "immer";
import type {
BuilderPatch,
BuilderPatchChange,
} from "@webstudio-is/project-build/contracts";
import type { WebstudioData } from "@webstudio-is/sdk";
import type {
MarketplaceProduct,
ProjectSettings,
} from "@webstudio-is/project-build";
import * as builderStatePatch from "@webstudio-is/project-build/state";
import { serverSyncStore } from "./sync-stores";
type BuilderPatchData = WebstudioData & {
marketplaceProduct?: MarketplaceProduct;
projectSettings?: ProjectSettings;
};
const getNamespaceData = (
data: BuilderPatchData,
namespace: BuilderPatchChange["namespace"]
) => {
const namespaceData = data[namespace];
if (namespaceData !== undefined) {
return namespaceData;
}
throw Error(`Cannot apply patch for unavailable namespace "${namespace}"`);
};
const applyBuilderPatchPayloadMutable = (
data: BuilderPatchData,
payload: readonly BuilderPatchChange[]
) =>
builderStatePatch.applyBuilderPatchPayloadMutable(
(namespace) => getNamespaceData(data, namespace),
payload
);
const clonePatch = (patch: Patch | BuilderPatch): Patch =>
patch.op === "remove"
? { op: patch.op, path: [...patch.path] }
: {
op: patch.op,
path: [...patch.path],
value: structuredClone(patch.value),
};
const cloneBuilderPatch = (patch: BuilderPatch): BuilderPatch =>
patch.op === "remove"
? { op: patch.op, path: [...patch.path] }
: {
op: patch.op,
path: [...patch.path],
value: structuredClone(patch.value),
};
const createSyncChangesFromBuilderPatches = ({
payload,
revisePatches,
}: {
payload: readonly BuilderPatchChange[];
revisePatches: readonly Patch[];
}): Change[] => {
const changes = new Map<
BuilderPatchChange["namespace"],
{ patches: Patch[]; revisePatches: Patch[] }
>();
const getChange = (namespace: BuilderPatchChange["namespace"]) => {
const change = changes.get(namespace) ?? {
patches: [],
revisePatches: [],
};
changes.set(namespace, change);
return change;
};
for (const { namespace, patches } of payload) {
if (patches.length === 0) {
continue;
}
const change = getChange(namespace);
change.patches.push(...patches.map(cloneBuilderPatch));
}
for (const patch of revisePatches) {
const [namespace, ...path] = patch.path;
if (typeof namespace !== "string") {
continue;
}
const change = getChange(namespace as BuilderPatchChange["namespace"]);
change.revisePatches.push(clonePatch({ ...patch, path }));
}
return Array.from(changes, ([namespace, change]) => ({
namespace,
patches: change.patches,
revisePatches: change.revisePatches,
}));
};
export const createSyncChangesFromBuilderPatchPayload = ({
data,
payload,
}: {
data: BuilderPatchData;
payload: BuilderPatchChange[];
}): Change[] => {
const payloadForReversePatches = payload.map((change) => ({
namespace: change.namespace,
patches: change.patches.map(cloneBuilderPatch),
}));
const [, , revisePatches] = produceWithPatches(data, (draft) => {
applyBuilderPatchPayloadMutable(draft, payloadForReversePatches);
});
return createSyncChangesFromBuilderPatches({ payload, revisePatches });
};
export const createTransactionFromBuilderPatchPayload = ({
data,
payload,
}: {
data: BuilderPatchData;
payload: BuilderPatchChange[];
}) => {
serverSyncStore.createTransactionFromChanges(
createSyncChangesFromBuilderPatchPayload({ data, payload })
);
};