Generated UI
A plan normally returns data. A UI plan returns an html template
or render function instead.
There are two separate operations:
generateCodePlan(...)calls a model and returns JavaScript plan code. It does not run that code or call handlers. Generation is optional because you may write or obtain the plan elsewhere.runCodePlan(...)takes existing plan code, evaluates it with the selected executor, and dispatches its tool calls to your handlers. It does not call a model.
For generated UI, first ask the model for a UI plan, then run that plan with the html function available.
Generate a UI plan
Section titled “Generate a UI plan”import { generateCodePlan } from "gruntend-sdk/generate";
const { plan } = await generateCodePlan({
model,
tools,
task,
input,
ui: { kind: "tagged-html" },
});
generateCodePlan() returns a GeneratedCodePlan:
interface GeneratedCodePlan {
readonly summary: string;
readonly input: Record<string, unknown>;
readonly code: string;
}
summarydescribes what the model generated.inputis passed to the selected executor asinput.codeis the JavaScript function body passed torunCodePlan().
ui: { kind: "tagged-html" } changes the generation instructions so code returns UI. It does not create or execute the UI.
If you write the plan yourself, skip this generation step and pass your code string directly to runCodePlan().
Provide html at runtime
Section titled “Provide html at runtime”import { createHtmlTag } from "gruntend-sdk/ui";
const html = createHtmlTag();
const result = await gruntend.runCodePlan(plan.code, {
input: plan.input,
handlers,
ui: { html },
});
ui: { html } makes the tagged-template function available inside the selected executor. The plan can now use html alongside input and tools.
Return UI from the plan
Section titled “Return UI from the plan”A plan may return a template:
const { items } = await tools.menu.items.list({
menuId: input.menuId,
});
return html`<ul>
${items.map((item) => html`<li>${item.name}: $${item.price}</li>`)}
</ul>`;
For local state, return a render function. State is an ordinary JavaScript closure:
var count = 0;
function increment() {
count += 1;
}
return function render() {
return html`<button type="button" onclick=${increment}>
Count: ${count}
</button>`;
};
After a handler runs, the surface renders again.
Render charts with SVG
Section titled “Render charts with SVG”SVG remains declarative markup, so a plan can compute chart geometry with JavaScript and return it through the same html template:
var maximum = Math.max.apply(null, input.values);
return html`<svg viewBox="0 0 640 240" role="img" aria-label="Revenue by day">
${input.values.map(function (value, index) {
var height = (value / maximum) * 180;
return html`<rect
x=${40 + index * 70}
y=${200 - height}
width="44"
height=${height}
fill="#f54a00"
></rect>`;
})}
</svg>`;
The template compiler reconstructs markup through explicit element and attribute allowlists. The SVG profile permits static geometry and text (svg, g, path, rect, circle, ellipse, line, polyline, polygon, text, tspan, title, and desc). Text and attribute interpolations are escaped. The selected renderer decides how that compiled frame is committed to its target.
The compiler rejects scripts, styles, inline event strings, namespace attributes, foreignObject, images, use, animation, filter elements, unknown elements, malformed tags, external links, external paint URLs, and forged runtime or delegated-handler targets. Only root-relative or fragment href values are accepted.
Mount the result
Section titled “Mount the result”Choose a normal DOM element where the generated interface should appear:
<div id="generated-ui"></div>
Then create a renderer and mount the generated UI:
import { createDomPurifyGeneratedUiRenderer } from "gruntend-sdk/renderer/dom-purify";
import { createGeneratedUi } from "gruntend-sdk/ui";
if (result.status !== "done") throw new Error(result.error);
const element = document.querySelector("#generated-ui");
if (!(element instanceof HTMLElement)) {
throw new Error("Generated UI host not found.");
}
const ui = createGeneratedUi(result.result).unwrap();
const renderer = createDomPurifyGeneratedUiRenderer();
const session = renderer.mount(element, ui, {
onError: console.error,
});
// when the host is removed
session.destroy();
createGeneratedUi() turns the plan result into a small interface with render() and runHandler(). GeneratedUiRenderer is a first-class object strategy selected by the application, not by the model. The renderer remains fixed for the mounted session.
The DOMPurify renderer is Gruntend’s built-in browser renderer. It applies Gruntend’s explicit markup policy, sanitizes into a DocumentFragment, and commits it with replaceChildren(). Applications with a different target or commit strategy can implement GeneratedUiRenderer<TTarget> directly.
The renderer session:
- renders the template into
element - listens for
click,submit,input, andchangeevents on that element - finds the delegated handler created by expressions such as
onclick=${handler} - runs the original closure handler with a restricted event payload
- renders again after the handler finishes, so closure state appears in the DOM
The returned session controls that connection:
session.render()renders the current state againsession.update(nextUi)replaces the generated UI and renders itsession.runHandler(id)invokes a generated handler programmaticallysession.destroy()removes event listeners and stops future rendering
Framework adapters perform this same mounting and cleanup through their component lifecycle. See Renderers for the renderer contract and Framework adapters for component integration.
Event safety
Section titled “Event safety”onclick=${handler} is compiled to a delegated data-gr-click identifier. Executable inline event strings are rejected. Text and attribute interpolations are escaped.
Generated handlers receive a restricted event payload, not broad DOM access.
Mutations
Section titled “Mutations”Task wording determines the intended interaction. A task that asks only to “preview” a change remains read-only. Ask to “preview and let me apply” when the generated interface should include a confirmation action.
A mutation UI should first render the proposed change. When the task explicitly asks to apply, create, update, delete, duplicate, copy, move, or otherwise change application data, the generation prompt asks for both the preview and the action that completes it. Mutation tools run only after explicit confirmation from the person using the interface. Permissions and persistence still belong to the app-owned handler.