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 in the interpreter, 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/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 interpreter 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/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 interpreter. 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.
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 and mount the generated UI:
import { createGeneratedUi } from "gruntend/ui";
import { mountGeneratedUi } from "gruntend/ui/dom";
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 mount = mountGeneratedUi(element, ui);
// when the host is removed
mount.destroy();
createGeneratedUi() turns the plan result into a small interface with render() and runHandler(). mountGeneratedUi() then:
- 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 mount handle controls that connection:
mount.render()renders the current state againmount.update(nextUi)replaces the generated UI and renders itmount.runHandler(id)invokes a generated handler programmaticallymount.destroy()removes event listeners and stops future rendering
Framework adapters perform this same mounting and cleanup through their component lifecycle. See Framework adapters.
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”A mutation UI should first render the proposed change. Call mutation tools only after explicit confirmation from the person using the interface. Permissions and persistence still belong to the app-owned handler.