Demos / Widget round trip · Kettle & Co
Widget round trip
Widgets are rich UI in the chat that send the visitor’s choice back to the agent. On a coffee roaster’s product page the agent shows a grind picker; a tap goes back as a turn, and the page’s grind selector and brew card change in reply.
This demo shows widgets: rich UI the agent puts in the chat, which sends the visitor's choice back to it. Watch the strip above the product page. It lights when the grind picker appears in the chat, and says so again when your tap comes back as "Grind: …" and the agent sets the grind on the page.
Kettle & Co is a fictional coffee roaster, and this is the product page for one coffee: Huila, Colombia, a 250 g bag for $14. The roaster's agent sits in the rail beside it. The demo proves a widget round trip: a widget the agent shows in the chat sends a turn back to the agent when the visitor taps it, and the agent answers that turn by changing the page.
The agent is configuration, not code: five prompt fields, one function and one widget, defined in demos/kettle and applied to the org through the admin MCP. The model is claude-haiku-4-5, and the agent opens the embed surface only.
The widget sends a turn back
Ask "How should I grind it?" and the agent calls widget__kettle-grind-picker. That widget is an authored WidgetDoc: a short question and four Button components, Whole bean, Espresso, Filter and French press. Each button's click action is { "sendToAgent": "Grind: Filter" } (with its own grind). The page does nothing to draw it: the drop-in <mindset-agent> element renders the widget natively inside its own chat, themed with the roaster's colors through its --ch-* channels.
When the visitor taps Filter, the element posts "Grind: Filter" into the conversation as the visitor's next turn. No page code is involved in that hop: it is the widget's own action, carried by the element. The agent reads it like any message, which is the point of sendToAgent. The model decides what a choice means, so the same buttons could start a quote, a booking or a search.
The agent changes the page
The agent's policy says that a grind message means two calls in one response. The first is the page tool set_grind, which the page registers with configure({ pageTools }) on the element. Its handler checks the argument against the four grinds (a model can send "french-press" or "FILTER", and anything else gets an error string back), then sets the grind selector. The selected option rings once and the selector is marked "Set by the roaster", only when that call arrives. The bag's label changes to "Ground for Filter".
The second is function__kettle-brew-guide, a function whose first step is the roaster's brew guide as literal data: one row per grind with grams, milliliters, water temperature in Fahrenheit and brew time. Pure steps after it match the method by word (V60, Chemex and pour over all mean Filter), read cups whether the model sends a number or the text "2", and write one recipe line. The filter row returns "15 g to 250 ml at 200°F". The page listens for mindset:runtime-event and, on that function's tool_end, draws the brew card from the function's own output, so the recipe on the page is exactly what the function returned. The agent's reply quotes the same line.
The page tells the agent what it shows
With every turn the page sends situational awareness through setSituationalAwareness: the product, the price, the selected grind and who set it, and the cart. So when the visitor picks a grind on the page themselves, the agent knows. add_to_bag adds bags in the selected grind, coerces its quantity, caps it at six a call, and refuses with a plain reason until a grind is set, which the agent answers by showing the grind picker again.
Under the hood
The Under the hood tab shows each step as it happens: the widget call, the function call with its arguments and result, and both page tools. Nothing on the page that shows agent state lights up before the event that causes it.