Velloo documentation
Markdown for LLMs ↗MCP: tree mutations
compose (the JSX an app holds, read as data), update_props, move_node, remove_node, set_node_id, inspect, and update_viewport_presets, plus the diagnostics every tree write returns.
A screen has one tree. compose builds it from JSX; the other operations on this page target nodes within it via locators: @id references or index paths. Except for compose, they also work on snippet bodies via the virtual screenId form snippet:<id> (see Editing a snippet body).
Successful writes can carry advisory propWarnings and diagnostics. For a guided workflow, see Design a screen.
compose
Write JSX onto a screen: append one or more subtrees, replace the screen’s whole tree, or replace one node. Tags resolve against the screen’s library, the folder’s extensions, snippet names, and the app’s own components, so a whole feature card, or a whole page, lands in one call. The JSX is read as data. No JavaScript executes.
| Arg | Type | Required | Description |
|---|---|---|---|
screenId |
string | yes | A screen id. snippet:<id> is not accepted |
mode |
string | yes | "append" inserts JSX roots as siblings under parentPath; "replace" makes it the screen’s entire tree, or with path replaces one node |
jsx |
string | one of the two | The JSX itself |
file |
string | one of the two | An app file to read the JSX from: a .jsx, .tsx, .js, or .ts file inside the app, relative to the app root |
parentPath |
locator | no | Append only. Parent to insert under; default [], the screen root. Must be a component node |
index |
number | no | Append only. Insertion index in the parent’s children; default: after the last child |
path |
locator | no | Replace only. The node to replace; default: the whole tree |
{
"screenId": "landing",
"mode": "append",
"parentPath": "@hero",
"jsx": "<Box vellooId=\"hero-actions\" className=\"mt-6 flex gap-3\"><Button vellooId=\"hero-cta\">Get started</Button><Button variant=\"outline\">Read the docs</Button></Box>"
}
Pass exactly one of jsx and file. Passing parentPath or index with mode: "replace", or path with mode: "append", returns BadRequest. Rebuild a scanned placeholder screen with mode: "replace".
Replacing the whole tree of a screen that doesn’t exist creates it. The screen is added and placed on the default board (else the first, else a new “Screens” board) in a frame that follows its height, and the result reports it as created: { screen, board, frame, note }. A screenId one edit away from an existing screen’s is refused as the typo it probably is, with ScreenNotFound and the suggestion; call add_screen to make it on purpose.
The JSX an app holds
Send the JSX you would write for the app. compose parses it and evaluates it as data, then writes the result out as plain elements:
{
"screenId": "pricing",
"mode": "replace",
"jsx": "const plans = [{ name: 'Starter', price: 0 }, { name: 'Pro', price: 19, featured: true }];\n\nfunction Plan({ name, price, featured }) {\n return (\n <Card className={cn('p-6', featured && 'border-primary')}>\n <Heading level={3}>{name}</Heading>\n <Text>${price} / month</Text>\n {featured && <Badge>Most popular</Badge>}\n </Card>\n );\n}\n\nexport default function Pricing() {\n return <Box className=\"grid grid-cols-2 gap-6\">{plans.map((plan) => <Plan key={plan.name} {...plan} />)}</Box>;\n}"
}
It reads:
- Data above the markup.
constdeclarations, arrays and objects built from literals, template strings, and number and date formatters. - Lists and conditions.
.map,.filter,.sliceand the other pure array, string, and number methods a render uses;&&,||,??, and ternaries; spreads. - Class joiners.
cn(...)and its relatives are joined into one class string. - Components defined in the source. A local component is written out where it is used. A design holds elements, not functions; to keep one reusable, make it a snippet.
- A whole page module. Imports, directives, and types are stepped over, an imported component is a tag, and the default export is what gets composed.
- A tag is the component its import names.
Stackimported from"@mantine/core"is Mantine’sStack, not Velloo’s primitive of the same name, when the app’s catalog has that module and export. A tag the source does not import, or imports from somewhere the catalog does not know, follows the bare-name rule below. - The app’s own data files. Data and helpers imported from files inside the app (
import { reviews } from "@/lib/reviews", relative or through a tsconfig alias) are read from those files and evaluated the same way. A package’s code is never read, and nothing outside the app root. - Hooks with an initial value.
useState(0)reads as0: the design shows the screen’s first state. Mantine’suseDisclosure,useToggle,useInputState, anduseCounterare read the same way, at the value their call holds.
What a static design can’t hold is dropped or refused, and never silently:
- Event handlers and other function props are dropped, and
keyandrefare ignored. - A value nothing holds is refused by name, at its line and column: a
fetch,new Date(), an unknown hook, a package’s data. - A component type as a prop is kept as one.
<AppShell.Section component={ScrollArea}>or<Button component={Link}>storescomponentas{ "$type": { "$ref": "ScrollArea" } }, resolved like a tag (the module the source imports it from first). The canvas passes the component itself, andemit_codewritescomponent={ScrollArea}with its import. A name nothing defines or imports is refused, and the message names the prop it was given to.
sourceNotes in the result says what was done: which components were written out, which props were dropped, which hooks were read at their initial value.
Composing from a file
file reads a page straight from the app, whether the agent wrote it a moment ago or the app already had it:
{ "screenId": "reviews", "mode": "replace", "file": "app/reviews/page.tsx" }
A Next app-router page is composed inside the layout files above it, with the document shell removed, because the page file alone is the page without its header. sourceNotes names the layouts it was composed inside, or one that could not be read. A file that is outside the app, or not one of the four extensions, returns BadRequest.
Tags, attributes, and text
- Roots. Append accepts a fragment (
<>…</>) or bare sibling elements and inserts them in source order. Replace takes one root; a page whose markup is several root elements is placed in a plain<div>, andsourceNotessays so. - Tags are component ids from
list_components:Button,Card,CardHeader, an extension id, or a snippet’s PascalCase tag. A dotted tag is a tag too, but only where the catalog has that id — a repository component’s compound part (<Tabs.List>) or a qualified name (<Mantine.Button>). Library ids stay flat, so shadcn’s is<CardHeader>, not<Card.Header>. A lowercase tag is always the HTML element, whatever a variable of that name holds, and lowers toBox as="…", orHtml as="…"on the HTML provider. An unknown component tag is rejected with close-match suggestions; a tag that is both a component and a snippet is rejected as ambiguous. - Attribute values are quoted strings (
className="p-6", single or double quotes), values in braces (size={24},disabled={false},items={["A", "B"]},sx={{"p": 2}}, or any expression the rules above can evaluate), or an element (leftSection={<Icon name="Plus" />}), stored as a node-valued prop and rendered as the real element. A bare attribute (disabled) meanstrue. - Styling. On Tailwind channels, a
style="…"string is treated asclassName, including on repository components that declare className styling. Object style channels retain their native object shape. - Duplicate attributes are rejected.
- Text children become the component’s
childrenprop, with whitespace collapsed:<Button>Save</Button>. Settingchildrenboth as text and as an attribute is an error. Text that sits beside elements (<Button><Icon name="Plus" />Add</Button>) is stored as a text node and renders as bare text, with no wrapper element. vellooId="hero-cta"sets the node’s stable$id, addressable afterwards as"@hero-cta". It must be a string, start with a letter, and be unique on the screen. Assign one to anything you may touch again.
Read, edit, send back
get_screen with mode: "jsx" prints a screen, or the subtree at path, in this same form. Edit the text and return it with mode: "replace" and the same path:
{ "screenId": "home", "mode": "replace", "path": "@pricing", "jsx": "<Box vellooId=\"pricing\" className=\"grid grid-cols-3 gap-6\">…</Box>" }
Snippets in JSX
A snippet is placed by its PascalCase tag, derived from the snippet’s id or name, as list_components reports it (pricing-tier becomes <PricingTier />):
- Each attribute is an argument and must name a declared param; an undeclared name is rejected.
className, unless the snippet declares aclassNameparam, becomes the instance’s extra class suffix, appended to the body root.- Text children, or exactly one element child for a
node-typed param, fill a param namedchildren; a snippet without achildrenparam rejects children. - Every required param (no default and not optional) must be present.
vellooIdsets the instance’s$id.
{
"screenId": "pricing",
"mode": "append",
"parentPath": "@tiers",
"jsx": "<PricingTier vellooId=\"tier-pro\" name=\"Pro\" price={19} featured className=\"md:scale-105\" />"
}
Per-instance interior overrides are not set at placement; apply them afterwards with update_snippet_instance.
Result and errors
A single-root append returns { mode, path, root }, where path is the new node’s index path; a whole-tree replace returns { mode, screenId, replacedRootRef, root }, and a replace at path returns { mode, screenId, path, root }. root is { kind: "component" | "snippet", id }. Multiple-root append returns added mutation results and roots descriptors. Append can also report into, a description of the parent it landed in. Multiple roots are written in sequence: if a later insertion fails, earlier insertions remain and the error lists them.
Depending on the call, the result also carries:
| Field | When |
|---|---|
created |
The screen did not exist and was created and placed |
framesFitted |
A whole-tree replace resized the frames that follow this screen: [{ frame, h }] |
sourceNotes |
The source held something worth reporting: components written out, props dropped, hooks read, layouts composed inside |
appComponents |
A bare name was written as the app’s own component (see below) |
propWarnings, diagnostics |
There is something to fix |
A JSX problem returns BadRequest with issues: [{ message, offset, line, column }] pointing into the source. A host-app package that isn’t installed never blocks design; emit_code reports it later in componentsToInstall. Other errors: ScreenNotFound, InvalidPath, IdNotFound, and IdConflict.
A replace is written from what the agent last read. If the screen changed since that agent’s last get_screen (a person edited it on the canvas, the file changed on disk, or another session wrote it), a replace is refused with ScreenChangedSinceRead and nothing is written. The error lists the nodes that differ in changed and names the get_screen call that brings the agent up to date. A replace at path is refused only when the change is inside that node, and an agent’s own writes never count against it. A session that has not read the screen yet has its first replace let through.
A bare name, one the source does not import from a module in the app’s catalog, means Velloo’s own component. When one carries props only the app’s same-named component takes (<Text fw={700}> in a Mantine app) and exactly one app component of that name takes them, compose writes that component and lists what it read under appComponents.read. When more than one could be meant, the write is refused with ShadowedComponent, which names the qualified tag to write (<Mantine.Text>), and nothing is written.
update_props
Patch props and native styling on one or more nodes in a single atomic write: one lock, one broadcast, one undo step.
| Arg | Type | Required | Description |
|---|---|---|---|
screenId |
string | yes | Screen id, or snippet:<id> for a snippet body |
patches |
array | yes | One entry per node: { path, propPatch?, style? }. Length 1 for a single edit. Each entry needs propPatch, style, or both |
gesture |
string | no | Opaque id (max 64 characters) of a continuous drag; writes sharing it collapse into one undo step |
Within an entry:
-
propPatchshallow-merges into the node’s props.nullremoves a key;classNameis a prop like any other. -
stylerestyles through the screen’s framework-native style channel, applied afterpropPatch:- shadcn and Tailwind no-framework folders: a Tailwind
classNamestring, which replaces the node’sclassName(an empty string removes it) - MUI: an
sxobject - no-framework, inline-style channel: a plain
styleobject, themed viavar(--...)tokens
Object channels merge shallowly and an inner
nulldrops that key;style: nullclears the channel’s prop entirely. A payload whose shape doesn’t fit the channel returnsBadRequestnaming the expected shape; this is how an agent discovers the active channel. All style payloads are validated before anything is written. - shadcn and Tailwind no-framework folders: a Tailwind
{
"screenId": "landing",
"patches": [
{ "path": "@hero-cta", "propPatch": { "children": "Start free", "size": "lg" } },
{ "path": "@hero", "style": "flex flex-col items-center gap-6 py-24" }
]
}
The native operation requires patches; a single entry object sent bare as patches is lifted into a one-element array. The guided façade also accepts a single edit such as { "screenId": "landing", "path": "@hero-cta", "props": { "children": "Start free" } }, normalizing props or propPatch and optional style into one patch. Use the patches form on the full surface and for bulk edits.
Returns { paths }, the resolved index path of each entry in order. The result warns when a propPatch replaces a whole object prop (such as style) and names the keys it dropped, and when a propPatch is empty. A slot prop given a markup string (rightSection="<Badge…>") warns that it renders as literal text; pass an element instead. update_props patches component nodes only; targeting a snippet instance returns InvalidPath pointing at update_snippet_instance.
Patching inside a snippet instance
To change one node inside a single instance’s body, or an instance’s arguments or extra classes, use update_snippet_instance. To change a body node across every instance, use update_snippet with innerPatch.
| Operation | Scope |
|---|---|
update_props |
A component node on a screen, or in a snippet body via snippet:<id> |
update_snippet_instance |
One instance: its args, extra classes, or one node inside its body |
update_snippet with innerPatch |
The shared definition: every instance at once |
move_node
Move a node to a new parent.
| Arg | Type | Required | Description |
|---|---|---|---|
screenId |
string | yes | Screen id, or snippet:<id> |
fromPath |
locator | yes | Node to move |
toParent |
locator | yes | Destination parent |
toIndex |
number | no | Insertion index in the destination’s children; default: last |
Returns { newPath }. Errors: InvalidMove when moving the root, or moving a node into itself or one of its descendants; InvalidPath for a missing node or parent.
remove_node
Remove the node at a locator.
| Arg | Type | Required | Description |
|---|---|---|---|
screenId |
string | yes | Screen id, or snippet:<id> |
path |
locator | yes |
Returns { removedRef } naming what was removed. The root itself is never removed: passing the root path [] clears all of the root’s children and leaves the root in place. To rebuild a whole screen, prefer compose with mode: "replace".
set_node_id
Set, rename, or clear a node’s stable $id anchor. Once set, the node is addressable as "@<id>" in any locator.
| Arg | Type | Required | Description |
|---|---|---|---|
screenId |
string | yes | Screen id, or snippet:<id> |
path |
locator | yes | |
id |
string or null | yes | A leading letter, then letters, digits, _, or -; null clears |
Returns { path, id }. Per-screen uniqueness is enforced; collisions return IdConflict. Targets component nodes and snippet instances; param refs can’t carry ids.
inspect
Return the server-rendered HTML, resolved className list, $ref, and resolved props for a node; use it instead of guessing rendered output.
| Arg | Type | Required | Description |
|---|---|---|---|
screenId |
string | yes | |
path |
locator | yes | |
innerPath |
string | no | For a snippet instance: "@id", a dotted index path, or "" for the body root (the default). Ignored for plain components |
computed |
boolean | no | Also render the screen in a real browser and measure the node. Default false; it costs a render |
viewport |
object | no | Render size for computed, { w, h } ({ width, height } is accepted). Default: the folder’s Desktop preset |
mode |
string | no | Render variant for computed: "light", "dark", or "compare" |
theme |
string | no | Named theme for computed. Default: the theme the screen’s board pins |
Returns { ref, resolvedProps, classes, bodyHtml, note? }. The HTML comes from Velloo’s bundled library; where the screen mounts the app’s own components (see component_status), the canvas and captures show those instead, so measure them with computed: true.
When path resolves to a snippet instance, the body is rendered with its args, $overrides, and $extraClassName applied; innerPath drills into one body node. Omitting it on an instance inspects the body root and returns a note on how to drill in.
With computed: true the result adds computed: { viewport, path, tag, class?, rect: { x, y, w, h }, style, children } (the node’s rendered box and resolved computed styles, plus its direct children’s boxes) and any mount diagnostics. computed is null with a computedNote when the node renders nothing measurable, or when innerPath points inside a snippet body (body nodes aren’t separately addressable in the render; measure the instance or use render_snippet).
update_viewport_presets
Replace the folder’s viewport presets: the sizes offered when adding a frame, and the source of the default screenshot viewport (the preset whose name contains “desktop”, else the first). Send the complete list in display order; names must be distinct and at least one is required. Frames store their own w/h, so this never resizes an existing frame.
| Arg | Type | Required | Description |
|---|---|---|---|
presets |
array | yes | The complete list in display order: [{ name, w, h }], with positive integer w/h |
{
"presets": [
{ "name": "Desktop", "w": 1440, "h": 900 },
{ "name": "Mobile", "w": 390, "h": 844 }
]
}
Returns { presets }. At least one preset is required and names must be distinct (case-insensitive); violations return BadRequest. Frames store their own w/h, so this never resizes an existing frame.
Diagnostics on results
There is no separate audit or class-validation operation. Design checks run automatically and arrive as a diagnostics array on the result of the call that needs them, omitted entirely when there is nothing to fix. Each entry is { severity, code, path, message, suggestion? }, with path naming the affected node:
| Code | Severity | Meaning |
|---|---|---|
tailwind/invalid-class |
warning | A class does not compile under the Tailwind JIT, which knows the folder’s theme tokens and custom CSS |
tailwind/undefined-var |
warning | A class compiles but references a CSS variable the design system never declares, so it paints a fallback |
tailwind/v3 |
warning | The host app is on Tailwind v3 and the class has v4 semantics; suggestion carries the v3 spelling when there is one |
theme/raw-color |
warning | Color classes that won’t theme-flip in dark mode, with token suggestions. Listed for the first eight nodes, then summarized |
theme/text-tone |
warning | A Text inside a filled Button or Badge overrides its label color; use plain text or an inline span |
screen/opaque |
warning | The whole screen is one app component or extension, so its interior cannot be selected, edited, or varied; compose its constituent parts |
render/component-threw |
error | The component threw while rendering and the canvas shows a placeholder; the message usually names the parent it must sit inside |
render/server-fallback |
warning | On a screenshot, compare_to_url, or computed inspect: the capture renders Velloo’s bundled components rather than the app’s own, so don’t tune the design against it |
render/stand-ins |
warning | On compare_to_url: components in a mounted screen drawn by a stand-in rather than the real component |
repo/shadowed-by-velloo |
info | On compare_to_url: bare names that render Velloo’s component where the app has its own. A caveat about the score, not its cause |
compose, update_props, update_snippet_instance, add_screen, add_snippet, update_snippet, and batch check what they wrote; screenshot and emit_code repeat the check over the complete screen, and emit_snippet over the snippet body. The class and color checks apply to Tailwind style channels and recognize classes and variables from preview-entry stylesheets. Render and opaque-screen checks also apply to other channels.
A screen’s diagnostics are sent once, then only when they change: an unchanged set for the same screen comes back as diagnosticsUnchanged with its count instead of the full list.
Diagnostics never fail or roll back a write; treat them as a triage list, not a gate. A node that deliberately doesn’t flip (text over a photo scrim, a brand accent) is exempted from theme/raw-color with a truthy data-accent prop, such as data-accent="ok"; the exemption does not cascade to children.