Skip to main content

Typed components

Generative UI sends a component as two values: a name the client registered and a props JSON object. That is all the wire ever carries. What this page adds is a typed view over that pair, so the props are checked at compile time instead of at render time — plus a worked example of each of the 13 components chativa's @chativa/genui package registers out of the box.

Bind your own component

mekik.component<Props>(name) binds a registry name once and returns something callable. In .NET there is no per-component factory; you pass the name to Shuttle.Ui / Shuttle.Mount (the props dictionary is the parity-exact shape — see Parity → Languages).

// Declare once, next to wherever you document the component's props.
const weather = mekik.component<{ city: string; temp: number }>("weather");

weather(ctx, { city: "İzmir", temp: 24 }); // emit it
weather(ctx, { city: "İzmir", temp: 24 }, { id: "wx" }); // …with an explicit chunk id
weather.name; // "weather"

A typed component also gives you the other two places a name + props pair travels:

// .mount → a handle whose update() re-renders this same instance
const live = weather.mount(ctx, { city: "İzmir", temp: 24 });
live.update({ city: "İzmir", temp: 25 });

// .ref → the UiRef an interrupt mounts as its form
await mekik.approve(ctx, { title: "Correct?" }, { ui: weather.ref({ city: "İzmir", temp: 24 }) });

Chunk ids: updating in place

Every chunk emitter (text, ui, event) takes an optional id. The same id updates that element in place instead of appending a new one — that is how two instances of the same component stay distinct and individually updatable, and how a progress bar advances without stacking up thirteen bars.

mekik.ui(ctx, "order-card", { id: "ORD-1", status: "loading" }, { id: "ORD-1" });
mekik.ui(ctx, "order-card", { id: "ORD-2", status: "loading" }, { id: "ORD-2" }); // a second card
mekik.ui(ctx, "order-card", { id: "ORD-1", status: "ready" }, { id: "ORD-1" }); // updates the first

mount is the managed form: it mints a replay-stable id (taskId + call order, exactly like tool ids), so the replay pass after an interrupt re-emits the same id and the client updates the existing element rather than duplicating it. Omit ids entirely and the mapper assigns stream-scoped ones. Note that an explicit id also opts a chunk out of text-run coalescing (Generative UI → stream lifecycle).

chativa's built-in components

mekik.genui.* / GenUI.* are the same factory applied to the components chativa registers out of the box — no client-side registration needed. Each is a full typed component: callable, .mount-able, .ref-able (TS); in .NET the emitters live on GenUI, with GenUI.Names.* for the registry names and small builders (GenUI.Field, GenUI.Step, …) for the nested structures.

Text block

Markdown-capable prose inside the GenUI stream.

mekik.genui.text(ctx, { content: "**Done.** Your refund is on its way." });

Card

Title, optional image and description, optional action buttons.

mekik.genui.card(ctx, {
title: "ORD-42",
description: "2 items · $249.90",
image: "https://cdn.example/orders/ORD-42.png",
actions: [{ label: "Track", value: "/track ORD-42" }],
});

Form

Input fields with a submit button. A submit reaches the server as a genui_event — and when the form was mounted by an interrupt, it answers the pause (Human-in-the-loop).

mekik.genui.form(ctx, {
title: "Where should we ship it?",
buttonText: "Save",
fields: [
{ name: "address", label: "Address", type: "text", required: true },
{ name: "note", label: "Delivery note", type: "text", placeholder: "Optional" },
],
});

// …or as an interrupt's form, so the run waits for it:
const shipping = await mekik.approve<{ address: string }>(
ctx,
{ title: "Shipping details" },
{ ui: mekik.genui.form.ref({ fields: [{ name: "address", label: "Address", type: "text" }] }) },
);

Alert

A callout banner. variant: info | success | warning | error.

mekik.genui.alert(ctx, {
variant: "warning",
title: "Partial refund",
message: "One item is past its return window and was excluded.",
});

Quick replies

Tappable chips inside the stream. Tapping one sends its value as the next user turn. To pause the run on the chips instead, use mekik.choose.

mekik.genui.quickReplies(ctx, {
label: "Anything else?",
items: [
{ label: "Track my order", value: "/track" },
{ label: "Talk to a human", value: "/agent" },
],
});

List

An ordered or bulleted list, each entry with an optional icon and secondary line.

mekik.genui.list(ctx, {
title: "What happens next",
ordered: true,
items: [
{ text: "We receive the item", secondary: "1–3 business days" },
{ text: "Refund is issued", secondary: "Within 24h of receipt" },
],
});

Table

Columns plus rows of cells, in column order.

mekik.genui.table(ctx, {
title: "Recent orders",
columns: ["Order", "Date", "Total"],
rows: [
["ORD-42", "2026-07-14", 249.9],
["ORD-38", "2026-06-30", 89.0],
],
});

Rating

A star rating. A submit reaches the server as a genui_event; pass readonly to display a score rather than collect one.

mekik.genui.rating(ctx, { title: "How did we do?", maxStars: 5 });
mekik.genui.rating(ctx, { title: "Seller rating", value: 4.5, readonly: true });

Progress

A 0–100 bar. This is the component that most wants an id: mount it once, then advance it.

const bar = mekik.genui.progress.mount(ctx, { label: "Processing refund", value: 0 });
await mekik.tool(ctx, "verify", { id: order.id }, () => Orders.verify(order.id));
bar.update({ label: "Processing refund", value: 60, caption: "Verified" });
await mekik.tool(ctx, "refund", { id: order.id }, () => Payments.refund(order.id));
bar.update({ label: "Processing refund", value: 100, variant: "success" });

Date picker

A date input; a pick reaches the server as a genui_event. Dates are ISO YYYY-MM-DD strings.

mekik.genui.datePicker(ctx, { label: "Pick a delivery date", min: "2026-08-01", max: "2026-08-31" });

Chart

Bar, line, or pie, with one or more datasets.

mekik.genui.chart(ctx, {
type: "line",
title: "Spend, last 6 months",
labels: ["Feb", "Mar", "Apr", "May", "Jun", "Jul"],
datasets: [{ label: "You", data: [120, 90, 140, 80, 260, 249], color: "#7c3aed" }],
});

Steps

A tracker whose entries are done | active | pending. Re-emit with the same id to advance it.

const tracker = mekik.genui.steps.mount(ctx, {
steps: [
{ label: "Requested", status: "done" },
{ label: "In review", status: "active", description: "Usually under an hour" },
{ label: "Refunded", status: "pending" },
],
});
// later in the same turn
tracker.update({
steps: [
{ label: "Requested", status: "done" },
{ label: "In review", status: "done" },
{ label: "Refunded", status: "active" },
],
});

A grid of images with optional captions.

mekik.genui.imageGallery(ctx, {
columns: 3,
images: [
{ src: "https://cdn.example/kettle-1.png", alt: "Kettle, front", caption: "Front" },
{ src: "https://cdn.example/kettle-2.png", alt: "Kettle, side" },
],
});

Where to go next

  • Messages — the other rendering path: standalone transcript entries (image, card, carousel…).
  • Human-in-the-loop — mounting a form as an interrupt's ui, and mekik.choose for typed buttons that pause the run.
  • Generative UI — the chunk model these components ride on.