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).
- TypeScript
- .NET
// 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"
Shuttle.Ui(ctx, "weather", new Dictionary<string, object?> { ["city"] = "İzmir", ["temp"] = 24 });
Shuttle.Ui(ctx, "weather", props, id: "wx"); // …with an explicit chunk id
A typed component also gives you the other two places a name + props pair travels:
- TypeScript
- .NET
// .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 }) });
// Shuttle.Mount → a handle whose Update() re-renders this same instance
var live = Shuttle.Mount(ctx, "weather", new Dictionary<string, object?> { ["temp"] = 24 });
live.Update(new Dictionary<string, object?> { ["temp"] = 25 });
// GenUI.Ref → the UiRef an interrupt mounts as its form
await Shuttle.Approve<bool>(ctx, payload, ui: GenUI.Ref("weather", props));
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.
- TypeScript
- .NET
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
Shuttle.Ui(ctx, "order-card", Props("ORD-1", "loading"), id: "ORD-1");
Shuttle.Ui(ctx, "order-card", Props("ORD-2", "loading"), id: "ORD-2"); // a second card
Shuttle.Ui(ctx, "order-card", Props("ORD-1", "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.
- TypeScript
- .NET
mekik.genui.text(ctx, { content: "**Done.** Your refund is on its way." });
GenUI.Text(ctx, "**Done.** Your refund is on its way.");
Card
Title, optional image and description, optional action buttons.
- TypeScript
- .NET
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" }],
});
GenUI.Card(ctx,
title: "ORD-42",
description: "2 items · $249.90",
image: "https://cdn.example/orders/ORD-42.png",
actions: [GenUI.CardAction("Track", "/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).
- TypeScript
- .NET
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" }] }) },
);
GenUI.Form(ctx,
fields:
[
GenUI.Field("address", "Address", "text", required: true),
GenUI.Field("note", "Delivery note", "text", placeholder: "Optional"),
],
title: "Where should we ship it?",
buttonText: "Save");
// …or as an interrupt's form, so the run waits for it:
var shipping = await Shuttle.Approve<Dictionary<string, object?>>(
ctx,
new Dictionary<string, object?> { ["title"] = "Shipping details" },
ui: GenUI.FormRef([GenUI.Field("address", "Address", "text")]));
Alert
A callout banner. variant: info | success | warning | error.
- TypeScript
- .NET
mekik.genui.alert(ctx, {
variant: "warning",
title: "Partial refund",
message: "One item is past its return window and was excluded.",
});
GenUI.Alert(ctx,
message: "One item is past its return window and was excluded.",
variant: "warning",
title: "Partial refund");
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.
- TypeScript
- .NET
mekik.genui.quickReplies(ctx, {
label: "Anything else?",
items: [
{ label: "Track my order", value: "/track" },
{ label: "Talk to a human", value: "/agent" },
],
});
GenUI.QuickReplies(ctx,
items: [GenUI.QuickReply("Track my order", "/track"), GenUI.QuickReply("Talk to a human", "/agent")],
label: "Anything else?");
List
An ordered or bulleted list, each entry with an optional icon and secondary line.
- TypeScript
- .NET
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" },
],
});
GenUI.List(ctx,
items:
[
GenUI.Item("We receive the item", secondary: "1–3 business days"),
GenUI.Item("Refund is issued", secondary: "Within 24h of receipt"),
],
title: "What happens next",
ordered: true);
Table
Columns plus rows of cells, in column order.
- TypeScript
- .NET
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],
],
});
GenUI.Table(ctx,
columns: ["Order", "Date", "Total"],
rows: [["ORD-42", "2026-07-14", 249.9], ["ORD-38", "2026-06-30", 89.0]],
title: "Recent orders");
Rating
A star rating. A submit reaches the server as a genui_event; pass readonly to display a score rather than collect one.
- TypeScript
- .NET
mekik.genui.rating(ctx, { title: "How did we do?", maxStars: 5 });
mekik.genui.rating(ctx, { title: "Seller rating", value: 4.5, readonly: true });
GenUI.Rating(ctx, title: "How did we do?", maxStars: 5);
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.
- TypeScript
- .NET
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" });
GenUI.Progress(ctx, 0, label: "Processing refund", id: "refund-bar");
await Shuttle.Tool(ctx, "verify", p, () => Orders.Verify(order.Id));
GenUI.Progress(ctx, 60, label: "Processing refund", caption: "Verified", id: "refund-bar");
await Shuttle.Tool(ctx, "refund", p, () => Payments.Refund(order.Id));
GenUI.Progress(ctx, 100, label: "Processing refund", variant: "success", id: "refund-bar");
Date picker
A date input; a pick reaches the server as a genui_event. Dates are ISO YYYY-MM-DD strings.
- TypeScript
- .NET
mekik.genui.datePicker(ctx, { label: "Pick a delivery date", min: "2026-08-01", max: "2026-08-31" });
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.
- TypeScript
- .NET
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" }],
});
GenUI.Chart(ctx,
type: "line",
title: "Spend, last 6 months",
labels: ["Feb", "Mar", "Apr", "May", "Jun", "Jul"],
datasets: [GenUI.Dataset([120, 90, 140, 80, 260, 249], label: "You", color: "#7c3aed")]);
Steps
A tracker whose entries are done | active | pending. Re-emit with the same id to advance it.
- TypeScript
- .NET
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" },
],
});
GenUI.Steps(ctx,
[
GenUI.Step("Requested", "done"),
GenUI.Step("In review", "active", description: "Usually under an hour"),
GenUI.Step("Refunded", "pending"),
], id: "tracker");
// later in the same turn
GenUI.Steps(ctx,
[
GenUI.Step("Requested", "done"),
GenUI.Step("In review", "done"),
GenUI.Step("Refunded", "active"),
], id: "tracker");
Image gallery
A grid of images with optional captions.
- TypeScript
- .NET
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" },
],
});
GenUI.ImageGallery(ctx,
images:
[
GenUI.Image("https://cdn.example/kettle-1.png", alt: "Kettle, front", caption: "Front"),
GenUI.Image("https://cdn.example/kettle-2.png", alt: "Kettle, side"),
],
columns: 3);
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, andmekik.choosefor typed buttons that pause the run. - Generative UI — the chunk model these components ride on.