Skip to content
Gruntend v0.5.0 beta

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.

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;
}
  • summary describes what the model generated.
  • input is passed to the selected executor as input.
  • code is the JavaScript function body passed to runCodePlan().

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().

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.

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.

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.

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:

  1. renders the template into element
  2. listens for click, submit, input, and change events on that element
  3. finds the delegated handler created by expressions such as onclick=${handler}
  4. runs the original closure handler with a restricted event payload
  5. 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 again
  • session.update(nextUi) replaces the generated UI and renders it
  • session.runHandler(id) invokes a generated handler programmatically
  • session.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.

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.

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.