Codelab
Model

Element

A node in a page or component tree — Primitive or Component kind, with props, styles, and optional repeat.

An Element is one node in a page or component tree. Kind + props + styles, optional repeat and renderIf. It belongs to exactly one container — a page or a component, never both, never the app directly.

packages/modules/element/core/src/domain/element.model.types.ts packages/config/db/src/element.schema.ts

The canvas is this tree. There is no parallel source of truth.

What it is

Kind is a tagged union:

{ _tag: "Primitive", value: "section" }       # registry: HTML, coss, codelab.image, …
{ _tag: "Component", value: "<componentId>" } # user component; list_components first

Known primitive names must use _tag: "Primitive". You cannot pass "Button" as a Component.

packages/modules/element/kernel/src/element-kind.enum.ts packages/modules/element/core/src/application/add-element.schema.ts

FieldMeaning
propsKind-specific and custom values: children, href, className, instance fields, event prop slots. A slot is a literal or a {{…}} expression. Event slots store a FunctionModel (functionId + instanceParams).
stylesResponsive style state. MCP writes these with style_*, not add_element. On insert, bulk Tailwind goes in className.
repeatable / repeatSourceWhen true, the node repeats once per item of repeatSource — a complete {{…}} that resolves to an array. Inside the subtree: {{repeat.item}}, {{repeat.index}}, {{repeat.key}}. Render expands Repeat copies; you author one node.
renderIfTagged {{…}} boolean. Falsy → this node and its subtree are omitted. null = always render.
parentId / siblingPositionTree edges. You almost never set these raw — parentElementId + targetPosition on add/move.

packages/modules/element/core/src/domain/tree/tree.model.ts packages/modules/action/kernel/src/function.model.ts packages/modules/expression/core/src/domain/expression.namespaces.ts

The display name in outlines and search is aria-label → component name → kind value.

packages/modules/element/core/src/domain/element.model.ts

Tree mutations take:

container: { _tag: "Page", id: pageId }
        # or { _tag: "Component", id: componentId }

packages/modules/container/core/src/domain/container-kind.model.ts

How it relates

  • Page / component — the two containers. add_element with no parentElementId appends under that container's root.
  • Primitive vs Component kind — a primitive is a registry widget (div, Button, Dialog). A Component kind is an instance of a user component; its props are that instance's values.
  • Props / Function — settable keys are prop slots. Event slots store one FunctionModel; sequences live on a store Action.
  • App — theme tokens and kind styles paint every element of a kind. Per-element style_* is the last layer.

MCP tools

packages/modules/mcp/server/src/element.mcp.ts packages/modules/mcp/server/src/registry.mcp.ts

Read first

ToolWhen
list_element_typesIndex by category (layout, form, overlay, …). Call once per group; pass the returned kind to add_element. Do not pass category to add.
inspect_element_typeRequired before any composite primitive. Canonical composition + pitfalls.
list_examplesCopy-paste patterns by kind / intent when the inspect doc is not enough.
load_page_outline / load_component_outlineWhole tree. Heavy — prefer find when you can.
find_element_in_page / find_element_in_componentLabel / kind / text → ids.
inspect_elementOne node's props, styles, renderIf, repeat.

Write

ToolWhen
add_root_elementFirst node of an empty component tree. Pages already have body. Root kind = Primitive only.
add_elementOne child. parentElementId omitted → under the root.
add_elementsA subtree in one persist. Parent command before child; mint elementId on parents so later commands can point at them. Prefer this over N add_element calls.
update_propsProp slots: children, href, src, instance fields, expressions, FunctionModel bindings. Set/unset commands — not a full replace.
update_elementNode fields: kind, repeatable, repeatSource, renderIf.
style_*Incremental, breakpoint-aware style (style_layout, style_typography, style_spacing_padding, …). Discover keys with style_schema.
move_elementReparent / reorder. Same targetPosition rules. Cannot move the root or into itself.
remove_elementDelete nodes. mode: "withReparent" unwraps a wrapper and keeps children. This is not delete_component.
duplicate_element_treeDeep copy as the next sibling. New ids. Cannot duplicate the root.
bind_propInside a component: {{props.<key>}} onto a target prop.

add_element.props is an array of { key, value } with string / number / boolean only. Objects, expressions, and function bindings go on update_props after create. Use className, never class.

packages/modules/props/core/src/domain/props.schema.ts

add_element {
  container: { _tag: "Page", id: pageId },
  kind: { _tag: "Primitive", value: "section" },
  parentElementId: "…",          # omit → under body
  # targetPosition omitted → append
  props: [
    { key: "aria-label", value: "Hero" },
    { key: "className", value: "flex flex-col gap-4 p-6" }
  ]
}

Repeat:

update_element {
  elementId: "…",
  commands: [
    { op: "set", key: "repeatable", value: true },
    { op: "set", key: "repeatSource",
      value: { _tag: "Expression", source: "{{props.events}}" } }
  ]
}

packages/modules/element/adapter/src/ai/update-element.tool.ts

Event prop slot (one FunctionModel per slot — sequences live on store actions, not on the element):

update_props {
  elementId: "…",
  commands: [{
    op: "set",
    key: "onClick",
    value: {
      functionId: "set-property",
      instanceParams: { store: "rsvp", property: "modalOpen", value: true }
    }
  }]
}

packages/modules/action/kernel/src/function.model.ts

Gotchas

  • aria-label is required on add, and it is a real DOM attribute. 1–3 words of purpose: "Hero", "Pricing", "Close". Not "div", "section wrapper", or a visual description. The tree and find_element_* use it as the label. A gratuitous label on a node that already has visible text overrides that text in the accessibility tree — still set a purpose name; do not use it as a fake editor tag. (packages/modules/element/adapter/src/ai/add-element.tool.ts)
  • Omit targetPosition to append. targetPosition: N inserts at index N (the current N and everything after shift down). 0 = first. Passing children.length is the off-by-one: with 6 children (0..5), 5 lands before the last. To make it last, omit the field. Same rule on move_element. (packages/modules/element/core/src/application/add-element.schema.ts)
  • inspect_element_type before composites. Blocking for Radio / RadioGroup, SelectItem, MenuItem, Dialog (Trigger / Popup / Header / Title / Description / Panel / Footer / Close), Field + FieldLabel, InputGroup, AccordionTrigger, … Skip only unambiguous leaves (div, span, p, h1h6, a, img, hr, br). Wrong wrappers fail silently (stacked radios, popups outside their trigger). (packages/modules/mcp/server/src/registry.mcp.ts)
  • Three write tools, three jobs. update_props = prop slots (including children and function bindings). update_element = kind / repeat / renderIf. style_* = style. Commands are { op: "set" \| "unset", key, value? } — last write wins; never send the whole object. (packages/modules/mcp/server/src/element.mcp.ts)
  • Place it once. parentElementId + targetPosition on insert. Do not add then move_element. For a whole subtree, add_elements (parent before child). (packages/modules/element/adapter/src/ai/add-element.tool.ts)
  • repeatSource is one whole binding. {{props.events}} or {{stores.x.items}}, not a fragment. (packages/modules/element/core/src/application/update-element.application.ts)

On this page