mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-10-30 16:47:54 +09:00
## 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
203 lines
5.8 KiB
TypeScript
203 lines
5.8 KiB
TypeScript
// Mutation utilities own changing existing instances in the live tree: delete,
|
|
// reparent, wrap, unwrap, convert, and visibility toggles. Put operations that
|
|
// move or transform existing nodes here; use insert.ts for new content and
|
|
// slot.ts for shared Slot boundary rules.
|
|
import { toast } from "@webstudio-is/design-system";
|
|
import { builderApi } from "~/shared/builder-api";
|
|
import { showAttribute } from "@webstudio-is/react-sdk";
|
|
import { type Instance, isComponentDetachable } from "@webstudio-is/sdk";
|
|
import {
|
|
$isContentMode,
|
|
$isPreviewMode,
|
|
$registeredComponentMetas,
|
|
$selectedInstancePath,
|
|
$selectedPage,
|
|
$textEditingInstanceSelector,
|
|
selectInstance,
|
|
} from "../nano-states";
|
|
import type { InstancePath } from "@webstudio-is/project-build/runtime";
|
|
import { $instances, $props } from "../sync/data-stores";
|
|
import { $instanceTags } from "~/builder/features/style-panel/shared/model";
|
|
import {
|
|
type DroppableTarget,
|
|
type InstanceSelector,
|
|
} from "@webstudio-is/project-build/runtime";
|
|
import { canUnwrapInstancePath } from "@webstudio-is/project-build/runtime";
|
|
import { canDeleteInstanceInContentMode } from "@webstudio-is/project-build/runtime";
|
|
import { executeRuntimeMutation } from "./data";
|
|
|
|
export const reparentInstance = (
|
|
sourceInstanceSelector: InstanceSelector,
|
|
dropTarget: DroppableTarget
|
|
) => {
|
|
const result = executeRuntimeMutation({
|
|
id: "instances.reparent",
|
|
input: {
|
|
sourceInstanceSelector,
|
|
dropTarget,
|
|
},
|
|
});
|
|
if (result !== undefined) {
|
|
selectInstance(result.result.instanceSelector);
|
|
}
|
|
};
|
|
|
|
export const deleteInstanceBySelector = (
|
|
instanceSelector: undefined | InstanceSelector
|
|
) => {
|
|
if (instanceSelector === undefined || instanceSelector.length < 2) {
|
|
return;
|
|
}
|
|
return executeRuntimeMutation({
|
|
id: "instances.deleteBySelector",
|
|
input: { instanceSelector },
|
|
});
|
|
};
|
|
|
|
export const canUnwrapInstance = (instancePath: InstancePath) => {
|
|
return canUnwrapInstancePath({
|
|
instancePath,
|
|
rootInstanceId: $selectedPage.get()?.rootInstanceId,
|
|
instances: $instances.get(),
|
|
props: $props.get(),
|
|
metas: $registeredComponentMetas.get(),
|
|
});
|
|
};
|
|
|
|
export const toggleInstanceShow = (instanceId: Instance["id"]) => {
|
|
const showProp = Array.from($props.get().values()).find(
|
|
(prop) => prop.instanceId === instanceId && prop.name === showAttribute
|
|
);
|
|
if (showProp !== undefined && showProp.type !== "boolean") {
|
|
return;
|
|
}
|
|
executeRuntimeMutation({
|
|
id: "instances.updateProps",
|
|
input: {
|
|
updates: [
|
|
{
|
|
instanceId,
|
|
name: showAttribute,
|
|
type: "boolean",
|
|
value: showProp?.type === "boolean" ? !showProp.value : false,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
};
|
|
|
|
export const wrapInstance = (component: string, tag?: string) => {
|
|
const instancePath = $selectedInstancePath.get();
|
|
// global root or body are selected
|
|
if (instancePath === undefined || instancePath.length === 1) {
|
|
return;
|
|
}
|
|
try {
|
|
const [selectedItem] = instancePath;
|
|
const result = executeRuntimeMutation({
|
|
id: "instances.wrap",
|
|
input: {
|
|
instanceSelector: selectedItem.instanceSelector,
|
|
component,
|
|
tag,
|
|
},
|
|
});
|
|
if (result !== undefined) {
|
|
selectInstance(result.result.instanceSelector);
|
|
}
|
|
} catch (error) {
|
|
toast.error(
|
|
error instanceof Error ? error.message : "Cannot wrap instance"
|
|
);
|
|
}
|
|
};
|
|
|
|
export const convertInstance = (component: string, tag?: string) => {
|
|
const instancePath = $selectedInstancePath.get();
|
|
// global root or body are selected
|
|
if (instancePath === undefined || instancePath.length === 1) {
|
|
return;
|
|
}
|
|
const instanceTags = $instanceTags.get();
|
|
try {
|
|
const [selectedItem] = instancePath;
|
|
executeRuntimeMutation({
|
|
id: "instances.convert",
|
|
input: {
|
|
instanceSelector: selectedItem.instanceSelector,
|
|
component,
|
|
tag,
|
|
currentTag: instanceTags.get(selectedItem.instance.id),
|
|
},
|
|
});
|
|
} catch (error) {
|
|
toast.error(
|
|
error instanceof Error ? error.message : "Cannot convert instance"
|
|
);
|
|
}
|
|
};
|
|
|
|
export const unwrapInstance = () => {
|
|
const instancePath = $selectedInstancePath.get();
|
|
if (instancePath === undefined || !canUnwrapInstance(instancePath)) {
|
|
return;
|
|
}
|
|
try {
|
|
const [selectedItem] = instancePath;
|
|
const result = executeRuntimeMutation({
|
|
id: "instances.unwrap",
|
|
input: {
|
|
instanceSelector: selectedItem.instanceSelector,
|
|
},
|
|
});
|
|
if (result !== undefined) {
|
|
selectInstance(result.result.instanceSelector);
|
|
}
|
|
} catch (error) {
|
|
toast.error(
|
|
error instanceof Error ? error.message : "Cannot unwrap instance"
|
|
);
|
|
}
|
|
};
|
|
|
|
export const deleteSelectedInstance = () => {
|
|
if ($isPreviewMode.get()) {
|
|
return;
|
|
}
|
|
const textEditingInstanceSelector = $textEditingInstanceSelector.get();
|
|
const instancePath = $selectedInstancePath.get();
|
|
// cannot delete instance while editing
|
|
if (textEditingInstanceSelector) {
|
|
return;
|
|
}
|
|
if (instancePath === undefined || instancePath.length === 1) {
|
|
return;
|
|
}
|
|
const [selectedItem] = instancePath;
|
|
const selectedInstanceSelector = selectedItem.instanceSelector;
|
|
const instances = $instances.get();
|
|
if (!isComponentDetachable(selectedItem.instance.component)) {
|
|
toast.error(
|
|
"This instance can not be moved outside of its parent component."
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if ($isContentMode.get()) {
|
|
if (
|
|
canDeleteInstanceInContentMode({
|
|
instanceSelector: selectedInstanceSelector,
|
|
instances,
|
|
}) === false
|
|
) {
|
|
builderApi.toast.info("You can't delete this instance in content mode.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
const result = deleteInstanceBySelector(selectedInstanceSelector);
|
|
if (result !== undefined && result.noop === false) {
|
|
selectInstance(result.result.instanceSelector);
|
|
}
|
|
};
|